@compas/code-gen
Version:
Generate various boring parts of your server
234 lines (203 loc) • 7.42 kB
JavaScript
import { AppError, isNil } from "@compas/stdlib";
import {
errorsThrowCombinedError,
stringFormatNameForError,
} from "../utils.js";
import { structureRoutes } from "./routes.js";
import { structureResolveReference } from "./structure.js";
/**
* Check if all route invalidations are correct.
*
* @param {import("../generate.js").GenerateContext} generateContext
*/
export function routeInvalidationsCheck(generateContext) {
/** @type {Array<import("@compas/stdlib").AppError>} */
const errors = [];
for (const route of structureRoutes(generateContext)) {
if (route.invalidations.length === 0) {
continue;
}
try {
for (const invalidation of route.invalidations) {
routeInvalidationProcess(generateContext, route, invalidation);
}
} catch (/** @type {any} */ e) {
errors.push(e);
}
}
errorsThrowCombinedError(errors);
}
/**
*
* @param {import("../generate.js").GenerateContext} generateContext
* @param {import("../../types/advanced-types.d.ts").NamedType<import("../generated/common/types.d.ts").StructureRouteDefinition>} route
* @param {import("../generated/common/types.js").StructureRouteInvalidationDefinition} invalidation
*/
function routeInvalidationProcess(generateContext, route, invalidation) {
const targetGroup = generateContext.structure[invalidation.target.group];
if (isNil(targetGroup)) {
throw AppError.serverError({
message: `Invalidation from ${stringFormatNameForError(
route,
)} specifies an invalid target ${formatTarget(
invalidation.target,
)}. Valid targets are 'R.get()' and 'R.post().idempotent()' routes.`,
});
}
if (isNil(invalidation.target.name)) {
// The group is valid, which will automatically invalide all 'get' / idempotent
// routes that are available.
return;
}
/** @type {import("../generated/common/types.js").StructureRouteDefinition} */
// @ts-expect-error
const targetRoute = targetGroup?.[invalidation.target.name];
if (
isNil(targetRoute) ||
targetRoute.type !== "route" ||
(targetRoute.method !== "GET" &&
!(targetRoute.method === "POST" && targetRoute.idempotent))
) {
throw AppError.serverError({
message: `Invalidation from ${stringFormatNameForError(
route,
)} specificies invalid target ${targetRoute}. Valid targets are 'R.get()' and 'R.post().idempotent()' routes.`,
});
}
// The logic for shared params and query is the same, so we can simplify the logic
// below to operate the same way, regardless of params or query.
const sharedKeys = [];
if (invalidation.properties.useSharedParams) {
sharedKeys.push("params");
}
if (invalidation.properties.useSharedQuery) {
sharedKeys.push("query");
}
for (const specificationKey of sharedKeys) {
if (
isNil(route[specificationKey]) ||
isNil(targetRoute[specificationKey])
) {
// Either the currrent route or the target route doesn't have query / params, so
// nothing is shared.
continue;
}
const routeObject =
structureResolveReference(
generateContext.structure,
route[specificationKey],
// @ts-expect-error
)?.keys ?? {};
const targetRouteObject =
structureResolveReference(
generateContext.structure,
targetRoute[specificationKey],
// @ts-expect-error
)?.keys ?? {};
const sourceParams = Object.keys(routeObject);
const targetParams = Object.keys(targetRouteObject);
for (const targetParam of targetParams) {
if (
!isNil(
invalidation.properties.specification?.[specificationKey]?.[
targetParam
],
)
) {
// Key is already specified by the user in the specification.
continue;
}
if (!sourceParams.includes(targetParam)) {
// Source does not have a property with the same name.
continue;
}
invalidation.properties.specification = invalidation.properties
.specification ?? {
params: {},
query: {},
};
invalidation.properties.specification[specificationKey] =
invalidation.properties.specification[specificationKey] ?? {};
invalidation.properties.specification[specificationKey][targetParam] = [
specificationKey,
targetParam,
];
}
}
// Check for errors in the specification;
for (const specificationKey of ["params", "query"]) {
for (const key of Object.keys(
invalidation.properties.specification?.[specificationKey] ?? {},
)) {
const targetObject =
structureResolveReference(
generateContext.structure,
targetRoute[specificationKey],
// @ts-expect-error
)?.keys ?? {};
if (isNil(targetObject?.[key])) {
throw AppError.serverError({
message: `Invalidation from ${stringFormatNameForError(
route,
)} to ${stringFormatNameForError(
targetRoute,
)} has an invalid specification. The specified source '${JSON.stringify(
invalidation.properties?.specification?.[specificationKey]?.[key],
)}' or target '["${specificationKey}", "${key}"]' does not exists. Both should be defined on their appropriate routes. See the docs for constraints.`,
});
}
const sourceSpecification =
invalidation.properties?.specification?.[specificationKey]?.[key];
let sourceLevel = route;
let isIncorrect = false;
for (let i = 0; i < sourceSpecification.length; ++i) {
sourceLevel = sourceLevel?.[sourceSpecification[i]];
if (isNil(sourceLevel)) {
isIncorrect = true;
break;
}
if (
i === sourceSpecification.length - 1 && // @ts-expect-error
(sourceLevel?.type === "reference" ?
structureResolveReference(generateContext.structure, sourceLevel)
?.type
: sourceLevel?.type) === "object"
) {
// The last 'key' of the source specification should not be an object
isIncorrect = true;
break;
}
if (i !== sourceSpecification.length) {
sourceLevel = // @ts-expect-error
sourceLevel?.type === "reference" ?
structureResolveReference(
generateContext.structure,
sourceLevel,
// @ts-expect-error
)?.keys // @ts-expect-error
: sourceLevel?.keys;
}
}
if (isIncorrect) {
throw AppError.serverError({
message: `Invalidation from ${stringFormatNameForError(
route,
)} to ${stringFormatNameForError(
targetRoute,
)} has an invalid specification. The specified source '${JSON.stringify(
invalidation.properties?.specification?.[specificationKey]?.[key],
)}' or target '["${specificationKey}", "${key}"]' does not exists. Both should be defined on their appropriate routes. See the docs for constraints.`,
});
}
}
}
}
/**
*
* @param {import("../generated/common/types.js").StructureRouteInvalidationDefinition["target"]} target
*/
function formatTarget(target) {
return `(targetGroup: '${target.group}'${
target.name ? `targetRoute: '${target.name}'` : ""
})`;
}