Skip to content

Commit

Permalink
feat: add fastifyTypeProviderPlugin plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
toomuchdesign committed Sep 15, 2023
1 parent 61e4cfe commit c7b4a54
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/smooth-buckets-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openapi-ts-json-schema': minor
---

Add `fastifyTypeProviderPlugin` plugin
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,18 @@ Beside generating the expected schema files under `outputPath`, `openapiToTsJson
}
```

## Plugins

`metaData` output can be used to dynamically generate extra custom output based on the generated schemas.

`openapi-ts-json-schema` currently ships with one plugin specifically designed to better integrate with [Fastify](https://fastify.dev/).

Read more about plugins in the [dedicated readme](./docs/plugins.md).

## Todo

- Consider merging "operation" and "path" parameters definition
- Consider removing required `definitionPathsToGenerateFrom` option in favour of exporting the whole OpenAPI definitions based on the structure defined in specs
- Explore how to preserve `$ref` values and rely on [`json-schema-to-ts` references generic to](https://www.npmjs.com/package/json-schema-to-ts#references) infer types

[ci-badge]: https://github.com/toomuchdesign/openapi-ts-json-schema/actions/workflows/ci.yml/badge.svg
[ci]: https://github.com/toomuchdesign/openapi-ts-json-schema/actions/workflows/ci.yml
Expand Down
75 changes: 75 additions & 0 deletions docs/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Plugins

## Fastify type provider plugin

This plugin generate the necessary connective tissue to optimally integrate `openapi-ts-json-schema` output with Fastify's [`json-schema-to-ts` type provider](https://github.com/fastify/fastify-type-provider-json-schema-to-ts) preserving JSON schemas `$ref`s.

The plugin generates a `fastifyTypeProvider.ts` file under `outputPath` exposing:

- `referenceSchemas`: an array containing all the `$ref` schemas found with relevant `$id` property ready to be registered with [`fastify.addSchema`](https://fastify.dev/docs/latest/Reference/Server/#addschema)
- `References` TS type specifically built to enable `json-schema-to-ts` to resolve `$ref` schema types

Generate TypeScript JSON schemas:

```ts
import {
openapiToTsJsonSchema,
fastifyTypeProviderPlugin,
} from 'openapi-ts-json-schema';

const { outputPath, metaData } = await openapiToTsJsonSchema({
openApiSchema: path.resolve(fixtures, 'path/to/open-api-spec.yaml'),
outputPath: 'path/to/generated/schemas',
definitionPathsToGenerateFrom: ['components.schemas', 'paths'],
refHandling: 'keep',
});

await fastifyTypeProviderPlugin({ outputPath, metaData });
```

Setup `Fastify` and `json-schema-to-ts` type provider:

```ts
import fastify from 'fastify';
import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
import {
References,
referenceSchemas,
} from 'path/to/generated/schemas/fastifyTypeProvider';

// Enable @fastify/type-provider-json-schema-to-ts to resolve all found `$ref` schema types
const server =
fastify().withTypeProvider<
JsonSchemaToTsProvider<JsonSchemaToTsProvider<{ references: References }>>
>();

/**
* Register `$ref` schemas individually so that they `$ref`s get resolved runtime.
* This also enables @fastify.swagger to re-expose the schemas as shared components.
*/
referenceSchemas.forEach((schema) => {
fastify.addSchema(schema);
});

// Reference the shared schema like the following
fastify.get(
'/profile',
{
schema: {
body: {
type: 'object',
properties: {
user: {
$ref: '#/components/schemas/User',
},
},
required: ['user'],
},
} as const,
},
(req) => {
// givenName and familyName will be correctly typed as strings!
const { givenName, familyName } = req.body.user;
},
);
```
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { openapiToTsJsonSchema } from './openapiToTsJsonSchema';
export { default as fastifyTypeProviderPlugin } from './plugins/fastifyTypeProviderPlugin';
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import { makeRelativePath, formatTypeScript, saveFile } from '../utils';
import type { Plugin, SchemaMetaData } from '../types';

const FILE_NAME = 'refTypesAsArray.ts';
const FILE_NAME = 'fastifyTypeProvider.ts';

const generateRefTypesAsArrayPlugin: Plugin = async ({
outputPath,
metaData,
}) => {
const refs: SchemaMetaData[] = [];
const fastifyTypeProviderPlugin: Plugin = async ({ outputPath, metaData }) => {
const refSchemaMetaData: SchemaMetaData[] = [];
metaData.schemas.forEach((schema) => {
if (schema.isRef) {
refs.push(schema);
refSchemaMetaData.push(schema);
}
});

const schemas = refs.map(
const schemas = refSchemaMetaData.map(
({ schemaAbsoluteImportPath, schemaUniqueName, schemaId }) => {
return {
importPath: makeRelativePath({
Expand All @@ -39,14 +36,20 @@ const generateRefTypesAsArrayPlugin: Plugin = async ({
output += `\n const ${schema.schemaUniqueName}WithId = {...${schema.schemaUniqueName}, $id: "${schema.schemaId}"} as const;`;
});

// Generate a TS tuple type containing the types of all $ref schema found
output += `\n\n
type RefTypes = [
export type References = [
${schemas
.map((schema) => `typeof ${schema.schemaUniqueName}WithId`)
.join(',')}
];`;

output += '\n\nexport default RefTypes';
// Generate an array af all $ref schema
// @TODO make selected schemas configurable
output += `\n\n
export const referenceSchemas = [
${schemas.map((schema) => `${schema.schemaUniqueName}WithId`).join(',')}
];`;

const formattedOutput = await formatTypeScript(output);
await saveFile({
Expand All @@ -55,4 +58,4 @@ const generateRefTypesAsArrayPlugin: Plugin = async ({
});
};

export default generateRefTypesAsArrayPlugin;
export default fastifyTypeProviderPlugin;
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,36 @@ import path from 'path';
import fs from 'fs/promises';
import { describe, it, expect } from 'vitest';
import { openapiToTsJsonSchema } from '../../src';
import generateRefTypesAsArrayPlugin from '../../src/plugins/generateRefTypesAsArrayPlugin';
import { fastifyTypeProviderPlugin } from '../../src';
import { importFresh } from '../test-utils';
import { formatTypeScript } from '../../src/utils';

const fixtures = path.resolve(__dirname, '../fixtures');

describe('generateRefTypesAsArrayPlugin plugin', async () => {
describe('fastifyTypeProviderPlugin plugin', () => {
it('generates expected file', async () => {
const { outputPath, metaData } = await openapiToTsJsonSchema({
openApiSchema: path.resolve(fixtures, 'complex/specs.yaml'),
outputPath: path.resolve(
fixtures,
'complex/schemas-autogenerated-fastifyTypeProviderPlugin',
),
definitionPathsToGenerateFrom: ['components.months', 'paths'],
refHandling: 'keep',
silent: true,
});

await generateRefTypesAsArrayPlugin({ outputPath, metaData });
await fastifyTypeProviderPlugin({ outputPath, metaData });

const actual = await fs.readFile(
path.resolve(outputPath, 'refTypesAsArray.ts'),
const actualAsText = await fs.readFile(
path.resolve(outputPath, 'fastifyTypeProvider.ts'),
{
encoding: 'utf8',
},
);

// @TODO find a better way to assert against generated types
const expected = await formatTypeScript(`
const expectedAsText = await formatTypeScript(`
import componentsSchemasAnswer from "./components/schemas/Answer";
import componentsMonthsJanuary from "./components/months/January";
import componentsMonthsFebruary from "./components/months/February";
Expand All @@ -45,14 +49,38 @@ describe('generateRefTypesAsArrayPlugin plugin', async () => {
$id: "#/components/months/February",
} as const;
type RefTypes = [
export type References = [
typeof componentsSchemasAnswerWithId,
typeof componentsMonthsJanuaryWithId,
typeof componentsMonthsFebruaryWithId,
];
export default RefTypes;`);
export const referenceSchemas = [
componentsSchemasAnswerWithId,
componentsMonthsJanuaryWithId,
componentsMonthsFebruaryWithId,
]`);

expect(actual).toBe(expected);
expect(actualAsText).toBe(expectedAsText);

// Ref schemas for fastify.addSchema
const answerSchema = await importFresh(
path.resolve(outputPath, 'components/schemas/Answer'),
);
const januarySchema = await importFresh(
path.resolve(outputPath, 'components/months/January'),
);
const februarySchema = await importFresh(
path.resolve(outputPath, 'components/months/February'),
);
const actualParsed = await importFresh(
path.resolve(outputPath, 'fastifyTypeProvider'),
);

expect(actualParsed.referenceSchemas).toEqual([
{ ...answerSchema.default, $id: '#/components/schemas/Answer' },
{ ...januarySchema.default, $id: '#/components/months/January' },
{ ...februarySchema.default, $id: '#/components/months/February' },
]);
});
});
8 changes: 8 additions & 0 deletions test/refHandling-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ describe('refHandling option === "import"', () => {
it('Generates expected schema', async () => {
const { outputPath } = await openapiToTsJsonSchema({
openApiSchema: path.resolve(fixtures, 'complex/specs.yaml'),
outputPath: path.resolve(
fixtures,
'complex/schemas-autogenerated-refHandling-import',
),
definitionPathsToGenerateFrom,
silent: true,
refHandling: 'import',
Expand Down Expand Up @@ -111,6 +115,10 @@ describe('refHandling option === "import"', () => {
it('Generates expected $ref schemas', async () => {
const { outputPath } = await openapiToTsJsonSchema({
openApiSchema: path.resolve(fixtures, 'complex/specs.yaml'),
outputPath: path.resolve(
fixtures,
'complex/schemas-autogenerated-refHandling-import',
),
definitionPathsToGenerateFrom: ['paths'],
silent: true,
refHandling: 'import',
Expand Down
5 changes: 4 additions & 1 deletion test/test-utils/importFresh.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// https://github.com/small-tech/import-fresh
/**
* It doesn't work for sub-imports
* https://github.com/small-tech/import-fresh
*/
export async function importFresh(absolutePathToModule: string) {
const cacheBustingModulePath = `${absolutePathToModule}?update=${Date.now()}`;
return await import(cacheBustingModulePath);
Expand Down
1 change: 1 addition & 0 deletions vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default defineConfig({
sequence: {
hooks: 'stack',
concurrent: false,
shuffle: true,
},
coverage: {
provider: 'istanbul',
Expand Down

0 comments on commit c7b4a54

Please sign in to comment.