-
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.
Merge pull request #15 from DigitalCommons/ts-restify-the-backend
Ts restify the backend
- Loading branch information
Showing
21 changed files
with
519 additions
and
382 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
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,8 @@ | ||
// Tell TS what this is | ||
declare const __BUILD_INFO__: { | ||
name: string; | ||
buildTime: string; | ||
version: number[]; | ||
commitDesc: string; | ||
nodeEnv: "development"|"production"; | ||
}; |
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 |
---|---|---|
@@ -1,53 +1,39 @@ | ||
// Fastify plugin autogenerated by fastify-openapi-glue | ||
import openapiGlue, { FastifyOpenapiGlueOptions } from "fastify-openapi-glue"; | ||
import * as dotenv from "dotenv"; | ||
import { Security } from "./security.js"; | ||
import { Service, ServiceOptions } from "./service.js"; | ||
import { specification } from "@mykomap/common"; | ||
import { | ||
FastifyInstance, | ||
FastifyPluginCallback, | ||
FastifyPluginOptions, | ||
} from "fastify"; | ||
import { MykomapRouterConfig, MykomapRouter } from "./routes.js"; | ||
import { initServer } from "@ts-rest/fastify"; | ||
import { contract } from "@mykomap/common"; | ||
import { FastifyInstance, FastifyPluginCallback } from "fastify"; | ||
import fp from "fastify-plugin"; | ||
|
||
dotenv.config(); | ||
|
||
interface MykomapApiPluginOptions extends FastifyPluginOptions { | ||
// Options for MykomapApiPlugin | ||
serviceOptions?: ServiceOptions; | ||
} | ||
|
||
// As per the guide fpr "Creating a TypeScript Fastify Plugin" | ||
// https://fastify.dev/docs/latest/Reference/TypeScript/#creating-a-typescript-fastify-plugin | ||
declare module "fastify" { | ||
interface FastifyRequest {} | ||
interface FastifyReply {} | ||
} | ||
|
||
export const pluginApi: FastifyPluginCallback<MykomapApiPluginOptions> = async ( | ||
/** Defines our API as a Fastify plugin */ | ||
const pluginApi: FastifyPluginCallback<MykomapRouterConfig> = async ( | ||
fastify: FastifyInstance, | ||
opts: MykomapApiPluginOptions, | ||
opts: MykomapRouterConfig, | ||
) => { | ||
const pluginOptions: FastifyOpenapiGlueOptions = { | ||
specification, | ||
serviceHandlers: new Service({ | ||
options: opts.serviceOptions, | ||
}), | ||
securityHandlers: new Security(), | ||
}; | ||
fastify.register(openapiGlue, { ...pluginOptions, ...opts }); | ||
}; | ||
// Tease apart Mykomap option from all others | ||
const { mykomap, ...fastifyOpts } = opts; | ||
|
||
// Get ts-rest's machinery going, | ||
const tsrServer = initServer(); | ||
|
||
// Feed our implementation to it, | ||
const router = tsrServer.router(contract, MykomapRouter({ mykomap })); | ||
|
||
// Plop out a Fastify plugin, | ||
const plugin = tsrServer.plugin(router); | ||
|
||
export const options: MykomapApiPluginOptions = { | ||
serviceOptions: { | ||
dataRoot: process.env.SERVER_DATA_ROOT ?? "data", | ||
}, | ||
ajv: { | ||
customOptions: { | ||
strict: false, | ||
}, | ||
}, | ||
// And register it. | ||
fastify.register(plugin, fastifyOpts); | ||
}; | ||
|
||
/** Export our prepared plugin */ | ||
export default fp(pluginApi); |
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,147 @@ | ||
import { TsRestResponseError } from "@ts-rest/core"; | ||
import { RouterImplementation } from "@ts-rest/fastify"; | ||
import { contract } from "@mykomap/common"; | ||
import { FastifyPluginOptions, FastifyReply, FastifyRequest } from "fastify"; | ||
import fs from "node:fs"; | ||
import path from "node:path"; | ||
|
||
/** Provides the shared configuration options for the Mykomap router implementation. */ | ||
export interface MykomapRouterConfig extends FastifyPluginOptions { | ||
/** Options specifically used by Mykomap Api plugin go in here */ | ||
mykomap: { | ||
/** Defines the path to the data store */ | ||
dataRoot: string; | ||
}; | ||
} | ||
|
||
////////////////////////////////////////////////////////////////////// | ||
// Helper types | ||
|
||
/** The Mykomap API contract type */ | ||
type Contract = typeof contract; | ||
|
||
////////////////////////////////////////////////////////////////////// | ||
// Helper functions | ||
|
||
/** Send a JSON file verbatim as Fastify's reply | ||
* | ||
* This function sends the file as a stream attached to the reply, which avoids | ||
* the need to load the file - which is potentially very large - deserialise it | ||
* from JSON, and then re-serialise it back to JSON. | ||
* | ||
* One consequence of this is that the content cannot be validated. You must | ||
* supply pre-validated files! | ||
* | ||
* @return true on success, false if the path doesn't correspond to a valid file | ||
* (in which case nothing can be sent).If successful, the reply will have | ||
* been modified to have an `application/json` content type, and the file | ||
* content attached as a stream. This reply can be returned from the handler | ||
* instead of a JSON object. | ||
*/ | ||
function sendJson(req: FastifyRequest, reply: FastifyReply, file: string) { | ||
req.log.debug(`data file path is '${file}`); | ||
|
||
if (!fs.existsSync(file)) return false; | ||
|
||
const stream = fs.createReadStream(file, "utf8"); | ||
reply.header("Content-Type", "application/json"); | ||
reply.send(stream); | ||
|
||
return true; | ||
} | ||
|
||
////////////////////////////////////////////////////////////////////// | ||
|
||
/** | ||
* Constructor for the MykoMap API router implementation. | ||
* | ||
* Accepts the router configuration, and returns an implementation using that configuration. | ||
* | ||
* See the Contract definition for details and documentation of the routes. | ||
* | ||
* Implementation Note: this is not a class, as ts-rest will attempt to recurse over its | ||
* members, and any non-router members seem to make it explode when it does that. | ||
* Therefore, we use a closure to store the configuration options, out of harm's way. | ||
*/ | ||
export function MykomapRouter( | ||
opts: MykomapRouterConfig, | ||
): RouterImplementation<Contract> { | ||
// Validation | ||
if (opts?.mykomap?.dataRoot == undefined) | ||
// deliberately loose check | ||
throw new Error("mandatory dataRoot option is not defined"); | ||
|
||
if (!fs.existsSync(opts.mykomap.dataRoot)) | ||
throw new Error( | ||
`the dataRoot plugin option is set but refers to a non-existing path: ` + | ||
`'${opts.mykomap.dataRoot}'.`, | ||
); | ||
|
||
// Concatenates the path components into an absolute file path | ||
const filePath = (...components: string[]): string => { | ||
const p = path.join(opts.mykomap.dataRoot ?? "", ...components) + ".json"; | ||
return p; | ||
}; | ||
|
||
// Construct and return the implementation object | ||
return { | ||
async getDataset({ params: { datasetId }, request, reply }) { | ||
// Validate the parameters some more | ||
|
||
if (!sendJson(request, reply, filePath("datasets", datasetId))) | ||
throw new TsRestResponseError(contract.getDataset, { | ||
status: 404, | ||
body: { message: `unknown datasetId '${datasetId}'` }, | ||
}); | ||
|
||
return reply; | ||
}, | ||
|
||
async searchDataset({ | ||
params: { datasetId }, | ||
query: { filter, text }, | ||
request, | ||
reply, | ||
}) { | ||
const filter2 = filter ?? []; | ||
|
||
const components = ["datasets", datasetId, "search", ...filter2, "text"]; | ||
if (text !== undefined) components.push(encodeURIComponent(text)); | ||
|
||
if (!sendJson(request, reply, filePath(...components))) | ||
return { | ||
status: 200, | ||
body: [], | ||
}; | ||
|
||
return reply; | ||
}, | ||
|
||
async getDatasetItem({ | ||
params: { datasetId, datasetItemId }, | ||
request, | ||
reply, | ||
}) { | ||
if ( | ||
!sendJson( | ||
request, | ||
reply, | ||
filePath("datasets", datasetId, "items", String(datasetItemId)), | ||
) | ||
) | ||
throw new TsRestResponseError(contract.getDatasetItem, { | ||
status: 404, | ||
body: { message: `item retrieve failed` }, | ||
}); | ||
|
||
return reply; | ||
}, | ||
|
||
async getVersion(req) { | ||
return { | ||
body: __BUILD_INFO__, | ||
status: 200, | ||
}; | ||
}, | ||
}; | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.