Skip to content

Commit

Permalink
fix/chore cleanup (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
Coobaha authored Dec 23, 2023
1 parent 5558aae commit 9f65a0d
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 104 deletions.
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
tap-snapshots/**/*.js
test/**/*.gen.json
CHANGELOG.md
10 changes: 0 additions & 10 deletions docs/Earthfile

This file was deleted.

1 change: 0 additions & 1 deletion docs/topics/CLI-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,3 @@ Glob pattern with watch
```shell
%cli% gen 'src/**/*_schema.ts' -w
```

46 changes: 18 additions & 28 deletions docs/topics/First-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ Use [CodeSanbox](https://codesandbox.io/p/github/Coobaha/typed-fastify-example?f
Try different requests `GET /`, `GET /?name=error` and `GET /?name=1&name=2`.
Also try changing the schema and use different types and implement new routes 🤓


## Define schema

Here is an example of an imaginary API schema:

```typescript
// example_schema.ts

import type { Schema } from "@coobaha/typed-fastify";
import type { Schema } from '@coobaha/typed-fastify';

interface User {
name: string;
Expand All @@ -38,7 +37,7 @@ interface ResponseError {

export interface ExampleSchema extends Schema {
paths: {
"GET /": {
'GET /': {
request: {
querystring: {
name?: string;
Expand All @@ -64,23 +63,23 @@ Create a service implementation that matches the schema:
```typescript
// example_service.ts

import { Service } from "@coobaha/typed-fastify";
import { Service } from '@coobaha/typed-fastify';

import type { ExampleSchema } from "./example_schema";
import type { ExampleSchema } from './example_schema';

const exampleService: Service<ExampleSchema> = {
"GET /": (req, reply) => {
const name = req.query.name ?? "John";
if (name === "error") {
'GET /': (req, reply) => {
const name = req.query.name ?? 'John';
if (name === 'error') {
return reply.status(404).send({
code: 404,
message: "Not Found",
message: 'Not Found',
});
}
return reply.status(200).send({
id: 1,
name: name,
});
});
},
};
```
Expand Down Expand Up @@ -109,10 +108,7 @@ npx tfs gen example_schema.ts
"properties": {
"User": {
"type": "object",
"required": [
"id",
"name"
],
"required": ["id", "name"],
"additionalProperties": false,
"properties": {
"name": {
Expand All @@ -125,10 +121,7 @@ npx tfs gen example_schema.ts
},
"ResponseError": {
"type": "object",
"required": [
"code",
"message"
],
"required": ["code", "message"],
"additionalProperties": false,
"properties": {
"message": {
Expand All @@ -146,9 +139,7 @@ npx tfs gen example_schema.ts
"GET /": {
"request": {
"type": "object",
"required": [
"querystring"
],
"required": ["querystring"],
"additionalProperties": false,
"properties": {
"querystring": {
Expand Down Expand Up @@ -177,23 +168,22 @@ npx tfs gen example_schema.ts

{collapsible="true" collapsed-title="Generated JSON Schema"}


## Add service to Fastify instance

We now have a JSON schema file `example_schema.gen.json` that we can use to validate requests and responses. To wire up
the service to Fastify, we need to add the schema and service to Fastify instance

> You can add this schema to version control and commit it to your repository.
> You can add this schema to version control and commit it to your repository.
> This way you can also verify diffs and always have the latest version of the schema in your repository.
>
>
> You can also configure file nesting in your Editor to hide generated files from the project view.
In the same file where you defined `exampleService`, add the following code:

```typescript
// example_service.ts

import jsonSchema from './example_schema.gen.json'
import jsonSchema from './example_schema.gen.json';

// ... other imports and exampleService implementation

Expand All @@ -210,16 +200,16 @@ addSchema(app, {
You can now run the server and try out the API.

It will have a:

1. Type safe implementation of the service, that matches the schema
2. Runtime validation of request and response
3. Generated JSON schema that allows you to use it for documentation and verification.

You can find the full example code here:

- Interactive playground [CodeSanbox](https://codesandbox.io/p/github/Coobaha/typed-fastify-example?file=%2Fsrc%2Fexample_schema.ts)
- Interactive playground [CodeSanbox](https://codesandbox.io/p/github/Coobaha/typed-fastify-example?file=%2Fsrc%2Fexample_schema.ts)
- [typed-fastify-example](https://github.com/coobaha/typed-fastify-example) on GitHub


<seealso style="links">
<category ref="external">
<a href="https://codesandbox.io/p/github/Coobaha/typed-fastify-example?file=%2Fsrc%2Fexample_schema.ts">CodeSanbox playground</a>
Expand Down
1 change: 0 additions & 1 deletion docs/topics/Installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ You can also add `--watch` flag to watch for changes in your schema files
> **To learn more about the cli**
>
> For more information about `tfs` command, see [CLI](CLI-Reference.md) page.
>
<seealso style="links">
<category ref="related">
Expand Down
1 change: 0 additions & 1 deletion docs/topics/Overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ Type safety end-to-end
: Package generates runtime validations based on your schema and enforces returning correct data at the type
level.


## What are the benefits?

- `%pkg%` adds strong TypeScript support to Fastify request handlers
Expand Down
82 changes: 42 additions & 40 deletions src/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,51 +26,53 @@ type JsonCastBehavior = 'cast' | 'combine';
type JsonlikeList<T extends unknown[], DoNotCastToPrimitive extends JsonCastBehavior> = T extends []
? []
: T extends [infer F, ...infer R]
? [NeverToNull<Jsonlike<F, DoNotCastToPrimitive>>, ...JsonlikeList<R, DoNotCastToPrimitive>]
: IsUnknown<T[number]> extends true
? []
: Array<T[number] extends NotJsonable ? null : Jsonlike<T[number], DoNotCastToPrimitive>>;
? [NeverToNull<Jsonlike<F, DoNotCastToPrimitive>>, ...JsonlikeList<R, DoNotCastToPrimitive>]
: IsUnknown<T[number]> extends true
? []
: Array<T[number] extends NotJsonable ? null : Jsonlike<T[number], DoNotCastToPrimitive>>;

// tweaked version of Jsonify from type-fest
export type Jsonlike<T, CastBehavior extends JsonCastBehavior> = T extends PositiveInfinity | NegativeInfinity
? null
: T extends NotJsonable
? IsNotJsonableError<'Passed value'>
: T extends JsonPrimitive
? T
: // Any object with toJSON is special case
T extends {
toJSON(): infer J;
}
? (() => J) extends () => JsonValue // Is J assignable to JsonValue?
? CastBehavior extends 'combine'
? T | J
: J // Then T is Jsonable and its Jsonable value is J
: Jsonlike<J, CastBehavior> // Maybe if we look a level deeper we'll find a JsonValue
: // Instanced primitives are objects
T extends Number
? number
: T extends String
? string
: T extends Boolean
? boolean
: T extends Map<any, any> | Set<any>
? EmptyObject
: T extends TypedArray
? Record<string, number> // Non-JSONable type union was found not empty
: T extends []
? []
: T extends unknown[]
? JsonlikeList<T, CastBehavior>
: T extends readonly unknown[]
? JsonlikeList<WritableDeep<T>, CastBehavior>
: T extends object
? {
[K in keyof T]: [T[K]] extends [NotJsonable] | [never] ? IsNotJsonableError<K> : Jsonlike<T[K], CastBehavior>; // JsonifyObject recursive call for its children
}
: T extends undefined
? T
: IsNotJsonableError<'Passed value'>;
? IsNotJsonableError<'Passed value'>
: T extends JsonPrimitive
? T
: // Any object with toJSON is special case
T extends {
toJSON(): infer J;
}
? (() => J) extends () => JsonValue // Is J assignable to JsonValue?
? CastBehavior extends 'combine'
? T | J
: J // Then T is Jsonable and its Jsonable value is J
: Jsonlike<J, CastBehavior> // Maybe if we look a level deeper we'll find a JsonValue
: // Instanced primitives are objects
T extends Number
? number
: T extends String
? string
: T extends Boolean
? boolean
: T extends Map<any, any> | Set<any>
? EmptyObject
: T extends TypedArray
? Record<string, number> // Non-JSONable type union was found not empty
: T extends []
? []
: T extends unknown[]
? JsonlikeList<T, CastBehavior>
: T extends readonly unknown[]
? JsonlikeList<WritableDeep<T>, CastBehavior>
: T extends object
? {
[K in keyof T]: [T[K]] extends [NotJsonable] | [never]
? IsNotJsonableError<K>
: Jsonlike<T[K], CastBehavior>; // JsonifyObject recursive call for its children
}
: T extends undefined
? T
: IsNotJsonableError<'Passed value'>;

export interface Invalid<msg = any> {
readonly __INVALID__: unique symbol;
Expand Down
44 changes: 22 additions & 22 deletions src/typed-fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,10 @@ export default addSchema;
type Missing<Candidate extends any, MaybeRequired extends any> = [Candidate, MaybeRequired] extends [never, never]
? false
: [Candidate] extends [never]
? true
: [Candidate] extends [MaybeRequired]
? false
: true;
? true
: [Candidate] extends [MaybeRequired]
? false
: true;

type ExtractMethodPath<T extends string | number | symbol, M extends F.HTTPMethods> = T extends `${M} ${infer P}`
? [M, P]
Expand All @@ -153,8 +153,8 @@ type MP<T extends string | number | symbol> =
type ExtractParams<T extends string | number | symbol, Acc = {}> = T extends `${infer _}:${infer P}/${infer R}`
? ExtractParams<R, Acc & { [_ in P]: string }>
: T extends `${infer _}:${infer P}`
? Id<Acc & { [_ in P]: string }>
: Acc;
? Id<Acc & { [_ in P]: string }>
: Acc;

type ArrayTOrT<T> = T | T[];

Expand Down Expand Up @@ -217,15 +217,15 @@ interface Reply<
...payload: [MissingStatus] extends [true]
? [Invalid<`Missing status`>]
: [MissingHeaders] extends [true]
? [
Invalid<`Missing headers: [ ${Extract<
keyof Omit<AllHeaders, keyof ([Headers] extends [never] ? {} : Headers)>,
string
>} ]. Please provide required headers before sending reply.`>,
]
: [Get2<Op['response'], Status, 'content'>] extends [never]
? []
: [Jsonlike<Get2<Op['response'], Status, 'content'>, 'combine'>]
? [
Invalid<`Missing headers: [ ${Extract<
keyof Omit<AllHeaders, keyof ([Headers] extends [never] ? {} : Headers)>,
string
>} ]. Please provide required headers before sending reply.`>,
]
: [Get2<Op['response'], Status, 'content'>] extends [never]
? []
: [Jsonlike<Get2<Op['response'], Status, 'content'>, 'combine'>]
): AsReply;

readonly request: Request<ServiceSchema, Op, Path, RawServer, RawRequest>;
Expand Down Expand Up @@ -360,11 +360,11 @@ type GetInvalidParamsValidation<
> = Router<Op>['Params'] extends never
? false
: IsEqual<DifferentKeys, {}> extends false
? Invalid<`request.params keys doesn't match params from router path, probably due to typo in [ ${Extract<
keyof DifferentKeys,
string
>} ] in path: [ ${Extract<MP<Path>[1], string>} ]`>
: false;
? Invalid<`request.params keys doesn't match params from router path, probably due to typo in [ ${Extract<
keyof DifferentKeys,
string
>} ] in path: [ ${Extract<MP<Path>[1], string>} ]`>
: false;

type Handler<
Op extends Operation,
Expand All @@ -381,8 +381,8 @@ type Handler<
ValidSchema = [Op['response'][keyof Op['response']]] extends [never]
? Invalid<`${Extract<Path, string>} - has no response, every path should have at least one response defined`>
: InvalidParams extends Invalid
? InvalidParams
: true,
? InvalidParams
: true,
> = ValidSchema extends true
? (
this: F.FastifyInstance<RawServer, RawRequest, RawReply, Logger>,
Expand Down
2 changes: 1 addition & 1 deletion test/tsconfig.test.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"noEmit": true,
"noEmit": true
},
"include": ["./**/*.ts", "../src"]
}

0 comments on commit 9f65a0d

Please sign in to comment.