@compas/code-gen
Version:
Generate various boring parts of your server
228 lines (201 loc) • 7.46 kB
JavaScript
import { isNil } from "@compas/stdlib";
import { structureIteratorNamedTypes } from "../../structure/structureIterators.js";
/**
* Validate all route invalidation specifications and work out usage of shared params and
* queries.
*
* @param {import("../../generated/common/types").CodeGenContext} context
*/
export function processRouteInvalidations(context) {
for (const route of structureIteratorNamedTypes(context.structure)) {
if (
route.type !== "route" ||
!("invalidations" in route) ||
route.invalidations.length === 0
) {
continue;
}
for (const invalidation of route.invalidations) {
processInvalidation(context, route, invalidation);
}
}
}
/**
* @param {import("../../generated/common/types").CodeGenContext} context
* @param {import("../../generated/common/types").CodeGenRouteType} route
* @param {import("../../generated/common/types").CodeGenRouteInvalidationType} invalidation
*/
function processInvalidation(context, route, invalidation) {
const targetGroup = context.structure[invalidation.target.group];
if (isNil(targetGroup)) {
context.errors.push({
errorString: `Invalidation from '${
route.uniqueName ?? ""
}' specifies an invalid target (group: '${invalidation.target.group}'${
invalidation.target.name ? `, name: '${invalidation.target.name}'` : ""
}).
Valid targets are 'R.get()' and 'R.post().idempotent()' routes.`,
});
return;
}
if (isNil(invalidation.target.name)) {
// Nothing else to do here, since react-query handles the rest.
// queryClient.invalidateQueries([invalidation.target.group]);
return;
}
/** @type {import("../../generated/common/types").CodeGenRouteType} */
// @ts-ignore
const targetRoute = targetGroup?.[invalidation.target.name];
if (
isNil(targetRoute) ||
targetRoute.type !== "route" ||
(targetRoute.method !== "GET" &&
!(targetRoute.method === "POST" && targetRoute.idempotent))
) {
context.errors.push({
errorString: `Invalidation from '${
route.uniqueName ?? ""
}' specifies an invalid target (group: '${invalidation.target.group}'${
invalidation.target.name ? `, name: '${invalidation.target.name}'` : ""
}).
Valid targets are 'R.get()' and 'R.post().idempotent()' routes.`,
});
return;
}
// Properties always default to an object with default properties. This makes the
// following logic easier, but not at generation time, since we need to check:
// (Object.keys(invalidation.properties.specification.params).length === 0 &&
// Object.keys(invalidation.properties.specification.query).length === 0)
const sharedKeys = [];
if (invalidation.properties.useSharedParams) {
sharedKeys.push("params");
}
if (invalidation.properties.useSharedQuery) {
sharedKeys.push("query");
}
// Below we use a bunch of `.reference?.keys ?? .keys`, because everything can be a
// reference.
for (const specificationKey of sharedKeys) {
if (
isNil(route[specificationKey]) ||
isNil(targetRoute[specificationKey])
) {
continue;
}
const routeObject =
route[specificationKey]?.reference?.keys ?? route[specificationKey]?.keys;
const targetRouteObject =
targetRoute[specificationKey]?.reference?.keys ??
targetRoute[specificationKey]?.keys;
if (!isNil(routeObject) && !isNil(targetRouteObject)) {
// @ts-ignore
const sourceParams = Object.keys(routeObject);
// @ts-ignore
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 =
targetRoute[specificationKey]?.reference?.keys ??
targetRoute[specificationKey]?.keys;
if (isNil(targetObject?.[key])) {
// Since we sort of have a valid match, we collect all specification errors and
// don't early return.
context.errors.push({
errorString: `Invalidation from '${
route.uniqueName ?? ""
}' to '(group: '${invalidation.target.group}'${
invalidation.target.name
? `, name: '${invalidation.target.name}'`
: ""
})' has an invalid specification.
The specified source ([${invalidation.properties.specification[
specificationKey
][key].join(", ")}]) or target ('${[
"specification",
specificationKey,
key,
].join(".")}') does not exist.
Both should be defined on their appropriate routes. See the docs for the constraints.`,
});
continue;
}
// TODO: Document constraints of source;
// - Should be object type or a reference to an object type
// - If nested, all nest levels should be objects;
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-ignore
(sourceLevel?.reference?.type ?? sourceLevel?.type) === "object"
) {
// The last 'key' of the source specification should not be an object
isIncorrect = true;
break;
}
if (i !== sourceSpecification.length) {
// @ts-ignore
sourceLevel = sourceLevel?.reference?.keys ?? sourceLevel?.keys;
}
}
if (isIncorrect) {
context.errors.push({
errorString: `Invalidation from '${
route.uniqueName ?? ""
}' to '(group: '${invalidation.target.group}'${
invalidation.target.name
? `, name: '${invalidation.target.name}'`
: ""
})' has an invalid specification.
The specified source ([${invalidation.properties.specification[
specificationKey
][key].join(", ")}]) or target ('${[
"specification",
specificationKey,
key,
].join(".")}') does not exist.
Both should be defined on their appropriate routes. See the docs for the constraints.`,
});
}
}
}
}