core/validators/metadata.validator.ts
core/validators/metadata.validator.ts is a file in Bane's Lab Build Scripts. 169 lines of code and 39 definitions.
import {
ARTICLE_MIN_PIXELS,
PAGE_DATE_FIELDS,
SCHEMA_FIELDS,
SCHEMA_KEYWORDS,
SCHEMA_PROPERTIES,
WEB_PAGE_TYPES,
} from "#configuration/constants/site.constants";
import type { Finding, PageArtifact } from "#types/validation.types";
import {
NO_ORGANIZATION,
missingSchemaField,
missingWebPage,
smallArticleImage,
undatedWebPage,
unknownSchemaProperty,
unknownSchemaType,
unreadableArticleImage,
unservedEncoding,
} from "#configuration/strings/site.strings";
import { isRecord } from "#core/selectors/base.selector";
const GRAPH_KEY = "@graph";
const TYPE_KEY = "@type";
const ORGANIZATION_TYPE = "Organization";
const CONTENT_URL_KEY = "contentUrl";
const ARTICLE_TYPE = "TechArticle";
const ISO_DAY_LENGTH = 10;
const TIME_MARK = "T";
const UTC_MARK = "Z";
const OFFSET_LENGTH = 6;
const OFFSET_SIGNS: ReadonlySet<string> = new Set(["+", "-"]);
const entitiesOf = function entitiesOf(schema: string | null): Record<string, unknown>[] {
if (schema === null) {
return [];
}
const parsed: unknown = JSON.parse(schema);
if (!isRecord(parsed) || !Array.isArray(parsed[GRAPH_KEY])) {
return [];
}
return parsed[GRAPH_KEY].filter(isRecord);
};
const isPageAt = function isPageAt(entity: Record<string, unknown>, address: string): boolean {
return WEB_PAGE_TYPES.has(String(entity[TYPE_KEY])) && entity["url"] === address;
};
const missingFieldsOf = function missingFieldsOf(file: string, entity: Record<string, unknown>): Finding[] {
const type = entity[TYPE_KEY];
const fields = typeof type === "string" ? (SCHEMA_FIELDS.get(type) ?? []) : [];
return fields
.filter((field) => entity[field] === undefined)
.map((field) => ({ file, message: missingSchemaField(String(type), field) }));
};
const typedEntitiesOf = function typedEntitiesOf(value: unknown): Record<string, unknown>[] {
if (Array.isArray(value)) {
return value.flatMap(typedEntitiesOf);
}
if (!isRecord(value)) {
return [];
}
const own = typeof value[TYPE_KEY] === "string" ? [value] : [];
return [...own, ...Object.values(value).flatMap(typedEntitiesOf)];
};
const foreignPropertiesOf = function foreignPropertiesOf(file: string, entity: Record<string, unknown>): Finding[] {
const type = String(entity[TYPE_KEY]);
const allowed = SCHEMA_PROPERTIES.get(type);
if (allowed === undefined) {
return [{ file, message: unknownSchemaType(type) }];
}
return Object.keys(entity)
.filter((property) => !SCHEMA_KEYWORDS.has(property) && !allowed.has(property))
.map((property) => ({ file, message: unknownSchemaProperty(type, property) }));
};
export const checkSchema = function checkSchema(file: string, artifact: PageArtifact, address: string): Finding[] {
const entities = entitiesOf(artifact.schema);
const findings: Finding[] = [];
if (!entities.some((entity) => entity[TYPE_KEY] === ORGANIZATION_TYPE)) {
findings.push({ file, message: NO_ORGANIZATION });
}
if (!entities.some((entity) => isPageAt(entity, address))) {
findings.push({ file, message: missingWebPage(address) });
}
return [
...findings,
...typedEntitiesOf(entities).flatMap((entity) => missingFieldsOf(file, entity)),
...typedEntitiesOf(entities).flatMap((entity) => foreignPropertiesOf(file, entity)),
];
};
const isIsoDay = function isIsoDay(value: string): boolean {
const day = new Date(value);
return !Number.isNaN(day.getTime()) && day.toISOString().slice(0, ISO_DAY_LENGTH) === value;
};
const hasOffset = function hasOffset(time: string): boolean {
return time.endsWith(UTC_MARK) || OFFSET_SIGNS.has(time.at(-OFFSET_LENGTH) ?? "");
};
const isIsoInstant = function isIsoInstant(value: unknown): boolean {
if (typeof value !== "string" || value.at(ISO_DAY_LENGTH) !== TIME_MARK) {
return false;
}
const time = value.slice(ISO_DAY_LENGTH);
return isIsoDay(value.slice(0, ISO_DAY_LENGTH)) && hasOffset(time) && !Number.isNaN(Date.parse(value));
};
const undatedFieldsOf = function undatedFieldsOf(
file: string,
entity: Record<string, unknown>,
fields: readonly string[],
): Finding[] {
return fields
.filter((field) => !isIsoInstant(entity[field]))
.map((field) => ({ file, message: undatedWebPage(field, String(entity[field])) }));
};
export const checkDates = function checkDates(file: string, artifact: PageArtifact, address: string): Finding[] {
const entities = entitiesOf(artifact.schema);
const page = entities.find((entity) => isPageAt(entity, address));
if (page === undefined) {
return [];
}
const dated = typedEntitiesOf(entities).filter((entity) => entity !== page);
return [
...undatedFieldsOf(file, page, PAGE_DATE_FIELDS),
...dated.flatMap((entity) =>
undatedFieldsOf(
file,
entity,
PAGE_DATE_FIELDS.filter((field) => entity[field] !== undefined),
),
),
];
};
const imagesOf = function imagesOf(entity: Record<string, unknown>): readonly string[] {
const { image } = entity;
const listed: readonly unknown[] = Array.isArray(image) ? image : [image];
return listed.filter((url): url is string => typeof url === "string");
};
export const checkArticleImages = function checkArticleImages(
file: string,
artifact: PageArtifact,
pixelsOf: (url: string) => number | null,
): Finding[] {
return typedEntitiesOf(entitiesOf(artifact.schema))
.filter((entity) => entity[TYPE_KEY] === ARTICLE_TYPE)
.flatMap(imagesOf)
.flatMap((url) => {
const pixels = pixelsOf(url);
if (pixels === null) {
return [{ file, message: unreadableArticleImage(url) }];
}
return pixels < ARTICLE_MIN_PIXELS
? [{ file, message: smallArticleImage(url, pixels, ARTICLE_MIN_PIXELS) }]
: [];
});
};
const contentUrlsOf = function contentUrlsOf(value: unknown): string[] {
if (Array.isArray(value)) {
return value.flatMap(contentUrlsOf);
}
if (!isRecord(value)) {
return [];
}
const own = typeof value[CONTENT_URL_KEY] === "string" ? [value[CONTENT_URL_KEY]] : [];
return [...own, ...Object.values(value).flatMap(contentUrlsOf)];
};
export const checkEncodings = function checkEncodings(
file: string,
artifact: PageArtifact,
isServed: (url: string) => boolean,
): Finding[] {
return contentUrlsOf(entitiesOf(artifact.schema))
.filter((url) => !isServed(url))
.map((url) => ({ file, message: unservedEncoding(url) }));
};