diff --git a/dotcom-rendering/scripts/json-schema/gen-schema.js b/dotcom-rendering/scripts/json-schema/gen-schema.js index 18215af79f0..5978a9dfeba 100644 --- a/dotcom-rendering/scripts/json-schema/gen-schema.js +++ b/dotcom-rendering/scripts/json-schema/gen-schema.js @@ -7,6 +7,7 @@ const { getNewsletterPageSchema, getTagPageSchema, getBlockSchema, + getEditionsCrosswordSchema, } = require('./get-schema'); const root = path.resolve(__dirname, '..', '..'); @@ -16,6 +17,7 @@ const frontSchema = getFrontSchema(); const tagPageSchema = getTagPageSchema(); const newsletterPageSchema = getNewsletterPageSchema(); const blockSchema = getBlockSchema(); +const editionsCrosswordSchema = getEditionsCrosswordSchema(); fs.writeFile( `${root}/src/model/article-schema.json`, @@ -71,3 +73,14 @@ fs.writeFile( } }, ); + +fs.writeFile( + `${root}/src/model/editions-crossword-schema.json`, + editionsCrosswordSchema, + 'utf8', + (err) => { + if (err) { + console.log(err); + } + }, +); diff --git a/dotcom-rendering/scripts/json-schema/get-schema.js b/dotcom-rendering/scripts/json-schema/get-schema.js index aa1c4a30fce..257edbeedf2 100644 --- a/dotcom-rendering/scripts/json-schema/get-schema.js +++ b/dotcom-rendering/scripts/json-schema/get-schema.js @@ -9,6 +9,7 @@ const program = TJS.getProgramFromFiles( path.resolve(`${root}/src/types/frontend.ts`), path.resolve(`${root}/src/types/tagPage.ts`), path.resolve(`${root}/src/types/newslettersPage.ts`), + path.resolve(`${root}/src/types/editionsCrossword.ts`), ], { skipLibCheck: true, @@ -57,10 +58,19 @@ const getBlockSchema = () => { ); }; +const getEditionsCrosswordSchema = () => { + return JSON.stringify( + TJS.generateSchema(program, 'FEEditionsCrosswords', settings), + null, + 4, + ); +}; + module.exports = { getArticleSchema, getFrontSchema, getTagPageSchema, getNewsletterPageSchema, getBlockSchema, + getEditionsCrosswordSchema, }; diff --git a/dotcom-rendering/src/model/editions-crossword-schema.json b/dotcom-rendering/src/model/editions-crossword-schema.json new file mode 100644 index 00000000000..82e1a0b11b2 --- /dev/null +++ b/dotcom-rendering/src/model/editions-crossword-schema.json @@ -0,0 +1,346 @@ +{ + "type": "object", + "properties": { + "quick": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "number": { + "type": "number" + }, + "date": { + "type": "string" + }, + "dimensions": { + "type": "object", + "properties": { + "cols": { + "type": "number" + }, + "rows": { + "type": "number" + } + }, + "required": [ + "cols", + "rows" + ] + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "number": { + "type": "number" + }, + "humanNumber": { + "type": "string" + }, + "direction": { + "enum": [ + "across", + "down" + ], + "type": "string" + }, + "position": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + }, + "required": [ + "x", + "y" + ] + }, + "separatorLocations": { + "type": "object", + "properties": { + ",": { + "type": "array", + "items": { + "type": "number" + } + }, + "-": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "length": { + "type": "number" + }, + "clue": { + "type": "string" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "solution": { + "type": "string" + }, + "format": { + "type": "string" + } + }, + "required": [ + "clue", + "direction", + "format", + "group", + "humanNumber", + "id", + "length", + "number", + "position", + "separatorLocations", + "solution" + ] + } + }, + "solutionAvailable": { + "type": "boolean" + }, + "hasNumbers": { + "type": "boolean" + }, + "randomCluesOrdering": { + "type": "boolean" + }, + "instructions": { + "type": "string" + }, + "creator": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "webUrl": { + "type": "string" + } + }, + "required": [ + "name", + "webUrl" + ] + }, + "pdf": { + "type": "string" + }, + "annotatedSolution": { + "type": "string" + }, + "dateSolutionAvailable": { + "type": "string" + } + }, + "required": [ + "date", + "dateSolutionAvailable", + "dimensions", + "entries", + "hasNumbers", + "name", + "number", + "pdf", + "randomCluesOrdering", + "solutionAvailable", + "type" + ] + }, + "cryptic": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "number": { + "type": "number" + }, + "date": { + "type": "string" + }, + "dimensions": { + "type": "object", + "properties": { + "cols": { + "type": "number" + }, + "rows": { + "type": "number" + } + }, + "required": [ + "cols", + "rows" + ] + }, + "entries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "number": { + "type": "number" + }, + "humanNumber": { + "type": "string" + }, + "direction": { + "enum": [ + "across", + "down" + ], + "type": "string" + }, + "position": { + "type": "object", + "properties": { + "x": { + "type": "number" + }, + "y": { + "type": "number" + } + }, + "required": [ + "x", + "y" + ] + }, + "separatorLocations": { + "type": "object", + "properties": { + ",": { + "type": "array", + "items": { + "type": "number" + } + }, + "-": { + "type": "array", + "items": { + "type": "number" + } + } + } + }, + "length": { + "type": "number" + }, + "clue": { + "type": "string" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "solution": { + "type": "string" + }, + "format": { + "type": "string" + } + }, + "required": [ + "clue", + "direction", + "format", + "group", + "humanNumber", + "id", + "length", + "number", + "position", + "separatorLocations", + "solution" + ] + } + }, + "solutionAvailable": { + "type": "boolean" + }, + "hasNumbers": { + "type": "boolean" + }, + "randomCluesOrdering": { + "type": "boolean" + }, + "instructions": { + "type": "string" + }, + "creator": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "webUrl": { + "type": "string" + } + }, + "required": [ + "name", + "webUrl" + ] + }, + "pdf": { + "type": "string" + }, + "annotatedSolution": { + "type": "string" + }, + "dateSolutionAvailable": { + "type": "string" + } + }, + "required": [ + "date", + "dateSolutionAvailable", + "dimensions", + "entries", + "hasNumbers", + "name", + "number", + "pdf", + "randomCluesOrdering", + "solutionAvailable", + "type" + ] + } + }, + "required": [ + "cryptic", + "quick" + ], + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/dotcom-rendering/src/model/validate.ts b/dotcom-rendering/src/model/validate.ts index 541d2ecf874..856c0816018 100644 --- a/dotcom-rendering/src/model/validate.ts +++ b/dotcom-rendering/src/model/validate.ts @@ -4,11 +4,13 @@ import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import type { FEFrontType } from '../../src/types/front'; import type { Block } from '../types/blocks'; +import type { FEEditionsCrosswords } from '../types/editionsCrossword'; import type { FEArticleType } from '../types/frontend'; import type { FENewslettersPageType } from '../types/newslettersPage'; import type { FETagPageType } from '../types/tagPage'; import articleSchema from './article-schema.json'; import blockSchema from './block-schema.json'; +import editionsCrosswordSchema from './editions-crossword-schema.json'; import frontSchema from './front-schema.json'; import newslettersPageSchema from './newsletter-page-schema.json'; import tagPageSchema from './tag-page-schema.json'; @@ -30,6 +32,9 @@ const validateAllEditorialNewslettersPage = ajv.compile( newslettersPageSchema, ); const validateBlock = ajv.compile(blockSchema); +const validateEditionsCrossword = ajv.compile( + editionsCrosswordSchema, +); export const validateAsArticleType = (data: unknown): FEArticleType => { if (validateArticle(data)) return data; @@ -43,6 +48,18 @@ export const validateAsArticleType = (data: unknown): FEArticleType => { ); }; +export const validateAsEditionsCrosswordType = ( + data: unknown, +): FEEditionsCrosswords => { + if (validateEditionsCrossword(data)) { + return data; + } + throw new TypeError( + `Unable to validate request body for editions crosswords.\n + ${JSON.stringify(validateEditionsCrossword.errors, null, 2)}`, + ); +}; + export const validateAsFrontType = (data: unknown): FEFrontType => { if (validateFront(data)) return data; diff --git a/dotcom-rendering/src/server/handler.editionsCrossword.ts b/dotcom-rendering/src/server/handler.editionsCrossword.ts new file mode 100644 index 00000000000..ee0b5ea9d78 --- /dev/null +++ b/dotcom-rendering/src/server/handler.editionsCrossword.ts @@ -0,0 +1,8 @@ +import type { RequestHandler } from 'express'; +import { validateAsEditionsCrosswordType } from '../model/validate'; + +export const handleEditionsCrossword: RequestHandler = ({ body }, res) => { + const editionsCrosswords = validateAsEditionsCrosswordType(body); + console.log(editionsCrosswords); + res.sendStatus(200); +}; diff --git a/dotcom-rendering/src/server/server.dev.ts b/dotcom-rendering/src/server/server.dev.ts index ca7ec1cef19..0e9c0fb3762 100644 --- a/dotcom-rendering/src/server/server.dev.ts +++ b/dotcom-rendering/src/server/server.dev.ts @@ -12,6 +12,7 @@ import { handleBlocks, handleInteractive, } from './handler.article.web'; +import { handleEditionsCrossword } from './handler.editionsCrossword'; import { handleFront, handleFrontJson, @@ -88,6 +89,8 @@ export const devServer = (): Handler => { return handleAppsInteractive(req, res, next); case 'AppsBlocks': return handleAppsBlocks(req, res, next); + case 'EditionsCrossword': + return handleEditionsCrossword(req, res, next); default: { // Do not redirect assets urls if (req.url.match(ASSETS_URL)) return next(); diff --git a/dotcom-rendering/src/server/server.prod.ts b/dotcom-rendering/src/server/server.prod.ts index 481eea244a0..7704baaeed3 100644 --- a/dotcom-rendering/src/server/server.prod.ts +++ b/dotcom-rendering/src/server/server.prod.ts @@ -20,6 +20,7 @@ import { handleBlocks, handleInteractive, } from './handler.article.web'; +import { handleEditionsCrossword } from './handler.editionsCrossword'; import { handleFront, handleFrontJson, @@ -80,6 +81,7 @@ export const prodServer = (): void => { app.post('/AppsArticle', logRenderTime, handleAppsArticle); app.post('/AppsInteractive', logRenderTime, handleAppsInteractive); app.post('/AppsBlocks', logRenderTime, handleAppsBlocks); + app.post('/EditionsCrossword', logRenderTime, handleEditionsCrossword); // These GET's are for checking any given URL directly from PROD app.get( diff --git a/dotcom-rendering/src/types/editionsCrossword.ts b/dotcom-rendering/src/types/editionsCrossword.ts new file mode 100644 index 00000000000..7950e21f2e9 --- /dev/null +++ b/dotcom-rendering/src/types/editionsCrossword.ts @@ -0,0 +1,43 @@ +export type FEEditionsCrosswords = { + quick: FEEditionsCrossword; + cryptic: FEEditionsCrossword; +}; + +type FECrosswordEntry = { + id: string; + number: number; + humanNumber: string; + direction: 'across' | 'down'; + position: { x: number; y: number }; + separatorLocations: { + ','?: number[]; + '-'?: number[]; + }; + length: number; + clue: string; + group: string[]; + solution: string; + format: string; +}; + +type FECrosswordDimensions = { + cols: number; + rows: number; +}; + +export type FEEditionsCrossword = { + name: string; + type: string; + number: number; + date: string; + dimensions: FECrosswordDimensions; + entries: FECrosswordEntry[]; + solutionAvailable: boolean; + hasNumbers: boolean; + randomCluesOrdering: boolean; + instructions?: string; + creator?: { name: string; webUrl: string }; + pdf: string; + annotatedSolution?: string; + dateSolutionAvailable: string; +};