diff --git a/apps/api/src/createApp.ts b/apps/api/src/createApp.ts index 0f4dae64..83ddf7a3 100644 --- a/apps/api/src/createApp.ts +++ b/apps/api/src/createApp.ts @@ -9,6 +9,9 @@ import authorsRoutes from "./routes/content/authors.ts"; import collectionsRoutes from "./routes/content/collections.ts"; import postRoutes from "./routes/content/post.ts"; import profilesRoutes from "./routes/content/profiles.ts"; +import schemaPostRoutes from "./routes/content/schema-post.ts"; +import schemaCollectionRoutes from "./routes/content/schema-collection.ts"; +import schemaAuthorRoutes from "./routes/content/schema-author.ts"; import fastify from "fastify"; import devRoutes from "./routes/dev/index.ts"; @@ -27,6 +30,9 @@ export const createApp = () => { app.register(collectionsRoutes); app.register(postRoutes); app.register(profilesRoutes); + app.register(schemaPostRoutes); + app.register(schemaCollectionRoutes); + app.register(schemaAuthorRoutes); if (env.ENVIRONMENT === "development") { app.register(devRoutes); diff --git a/apps/api/src/routes/content/createSchemaRoute.ts b/apps/api/src/routes/content/createSchemaRoute.ts new file mode 100644 index 00000000..e923e0c5 --- /dev/null +++ b/apps/api/src/routes/content/createSchemaRoute.ts @@ -0,0 +1,42 @@ +import type { FastifyPluginAsync } from "fastify"; +import { Type, type TSchema } from "typebox"; + +export const createSchemaRoute = ( + path: string, + description: string, + schema: TSchema, +): FastifyPluginAsync => { + const responseSchema = Type.Object( + {}, + { + additionalProperties: true, + description, + examples: [schema], + }, + ); + + return async (fastify) => { + fastify.get( + path, + { + schema: { + description, + response: { + 200: { + description: "Successful", + content: { + "application/json": { + schema: responseSchema, + }, + }, + }, + }, + }, + }, + async (_request, reply) => { + reply.code(200); + reply.send(schema); + }, + ); + }; +}; diff --git a/apps/api/src/routes/content/schema-author.test.ts b/apps/api/src/routes/content/schema-author.test.ts new file mode 100644 index 00000000..d39a6d2e --- /dev/null +++ b/apps/api/src/routes/content/schema-author.test.ts @@ -0,0 +1,27 @@ +import fastify, { type FastifyInstance } from "fastify"; +import schemaAuthorRoutes from "./schema-author.ts"; +import { AuthorMetaSchema } from "@playfulprogramming/common"; + +describe("Schema Author Routes Tests", () => { + let app: FastifyInstance; + beforeAll(async () => { + app = fastify(); + await app.register(schemaAuthorRoutes); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("/content/schema/author", () => { + test("returns the author frontmatter JSON Schema", async () => { + const response = await app.inject({ + method: "GET", + url: "/content/schema/author", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual(AuthorMetaSchema); + }); + }); +}); diff --git a/apps/api/src/routes/content/schema-author.ts b/apps/api/src/routes/content/schema-author.ts new file mode 100644 index 00000000..eb792daa --- /dev/null +++ b/apps/api/src/routes/content/schema-author.ts @@ -0,0 +1,10 @@ +import { AuthorMetaSchema } from "@playfulprogramming/common"; +import { createSchemaRoute } from "./createSchemaRoute.ts"; + +const schemaAuthorRoutes = createSchemaRoute( + "/content/schema/author", + "Fetch the JSON Schema for author frontmatter, as validated by the sync worker.", + AuthorMetaSchema, +); + +export default schemaAuthorRoutes; diff --git a/apps/api/src/routes/content/schema-collection.test.ts b/apps/api/src/routes/content/schema-collection.test.ts new file mode 100644 index 00000000..cbd0f17f --- /dev/null +++ b/apps/api/src/routes/content/schema-collection.test.ts @@ -0,0 +1,27 @@ +import fastify, { type FastifyInstance } from "fastify"; +import schemaCollectionRoutes from "./schema-collection.ts"; +import { CollectionMetaSchema } from "@playfulprogramming/common"; + +describe("Schema Collection Routes Tests", () => { + let app: FastifyInstance; + beforeAll(async () => { + app = fastify(); + await app.register(schemaCollectionRoutes); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("/content/schema/collection", () => { + test("returns the collection frontmatter JSON Schema", async () => { + const response = await app.inject({ + method: "GET", + url: "/content/schema/collection", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual(CollectionMetaSchema); + }); + }); +}); diff --git a/apps/api/src/routes/content/schema-collection.ts b/apps/api/src/routes/content/schema-collection.ts new file mode 100644 index 00000000..d8699d8f --- /dev/null +++ b/apps/api/src/routes/content/schema-collection.ts @@ -0,0 +1,10 @@ +import { CollectionMetaSchema } from "@playfulprogramming/common"; +import { createSchemaRoute } from "./createSchemaRoute.ts"; + +const schemaCollectionRoutes = createSchemaRoute( + "/content/schema/collection", + "Fetch the JSON Schema for collection frontmatter, as validated by the sync worker.", + CollectionMetaSchema, +); + +export default schemaCollectionRoutes; diff --git a/apps/api/src/routes/content/schema-post.test.ts b/apps/api/src/routes/content/schema-post.test.ts new file mode 100644 index 00000000..1630c2eb --- /dev/null +++ b/apps/api/src/routes/content/schema-post.test.ts @@ -0,0 +1,27 @@ +import fastify, { type FastifyInstance } from "fastify"; +import schemaPostRoutes from "./schema-post.ts"; +import { PostMetaSchema } from "@playfulprogramming/common"; + +describe("Schema Post Routes Tests", () => { + let app: FastifyInstance; + beforeAll(async () => { + app = fastify(); + await app.register(schemaPostRoutes); + }); + + afterAll(async () => { + await app.close(); + }); + + describe("/content/schema/post", () => { + test("returns the post frontmatter JSON Schema", async () => { + const response = await app.inject({ + method: "GET", + url: "/content/schema/post", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual(PostMetaSchema); + }); + }); +}); diff --git a/apps/api/src/routes/content/schema-post.ts b/apps/api/src/routes/content/schema-post.ts new file mode 100644 index 00000000..6d19a681 --- /dev/null +++ b/apps/api/src/routes/content/schema-post.ts @@ -0,0 +1,10 @@ +import { PostMetaSchema } from "@playfulprogramming/common"; +import { createSchemaRoute } from "./createSchemaRoute.ts"; + +const schemaPostRoutes = createSchemaRoute( + "/content/schema/post", + "Fetch the JSON Schema for post frontmatter, as validated by the sync worker.", + PostMetaSchema, +); + +export default schemaPostRoutes; diff --git a/apps/worker/src/tasks/sync-author/processor.ts b/apps/worker/src/tasks/sync-author/processor.ts index b32430de..3072db75 100644 --- a/apps/worker/src/tasks/sync-author/processor.ts +++ b/apps/worker/src/tasks/sync-author/processor.ts @@ -1,4 +1,4 @@ -import { env } from "@playfulprogramming/common"; +import { env, AuthorMetaSchema } from "@playfulprogramming/common"; import { Tasks, createJob } from "@playfulprogramming/bullmq"; import { db, @@ -10,7 +10,6 @@ import * as github from "@playfulprogramming/github-api"; import { s3 } from "@playfulprogramming/s3"; import { createProcessor } from "../../createProcessor.ts"; import matter from "gray-matter"; -import { AuthorMetaSchema } from "./types.ts"; import { Value } from "typebox/value"; import sharp from "sharp"; import { Readable } from "node:stream"; diff --git a/apps/worker/src/tasks/sync-collection/processor.ts b/apps/worker/src/tasks/sync-collection/processor.ts index 79ec0f1e..73a2224f 100644 --- a/apps/worker/src/tasks/sync-collection/processor.ts +++ b/apps/worker/src/tasks/sync-collection/processor.ts @@ -1,4 +1,4 @@ -import { env } from "@playfulprogramming/common"; +import { env, CollectionMetaSchema } from "@playfulprogramming/common"; import { Tasks, createJob } from "@playfulprogramming/bullmq"; import { collectionAuthors, @@ -11,7 +11,6 @@ import * as github from "@playfulprogramming/github-api"; import { createProcessor } from "../../createProcessor.ts"; import { eq } from "drizzle-orm"; import matter from "gray-matter"; -import { CollectionMetaSchema } from "./types.ts"; import { Value } from "typebox/value"; import sharp from "sharp"; import { Readable } from "node:stream"; diff --git a/apps/worker/src/tasks/sync-post/processor.ts b/apps/worker/src/tasks/sync-post/processor.ts index 91b9e8f8..2cf26c73 100644 --- a/apps/worker/src/tasks/sync-post/processor.ts +++ b/apps/worker/src/tasks/sync-post/processor.ts @@ -1,4 +1,4 @@ -import { env } from "@playfulprogramming/common"; +import { env, PostMetaSchema } from "@playfulprogramming/common"; import { Tasks, createJob } from "@playfulprogramming/bullmq"; import { db, @@ -18,7 +18,6 @@ import sharp from "sharp"; import { Readable } from "node:stream"; import { extname } from "node:path/posix"; import { Response } from "undici"; -import { PostMetaSchema } from "./types.ts"; import { extractLocale } from "../../utils/extractLocale.ts"; import { extractMarkdownExcerpt } from "../../utils/extractMarkdownExcerpt.ts"; diff --git a/apps/worker/src/tasks/sync-author/types.ts b/packages/common/src/frontmatter/author.ts similarity index 100% rename from apps/worker/src/tasks/sync-author/types.ts rename to packages/common/src/frontmatter/author.ts diff --git a/apps/worker/src/tasks/sync-collection/types.ts b/packages/common/src/frontmatter/collection.ts similarity index 100% rename from apps/worker/src/tasks/sync-collection/types.ts rename to packages/common/src/frontmatter/collection.ts diff --git a/apps/worker/src/tasks/sync-post/types.ts b/packages/common/src/frontmatter/post.ts similarity index 100% rename from apps/worker/src/tasks/sync-post/types.ts rename to packages/common/src/frontmatter/post.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 0561b3e3..d6c0508e 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1 +1,4 @@ export * from "./env.ts"; +export * from "./frontmatter/post.ts"; +export * from "./frontmatter/collection.ts"; +export * from "./frontmatter/author.ts";