UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

374 lines (331 loc) 10.7 kB
// @ts-nocheck import { isNil } from "@compas/stdlib"; import { structureIteratorNamedTypes } from "../../structure/structureIterators.js"; import { traverseType } from "../structure-traverser.js"; import { atomicUpdateFieldsTable } from "./update-type.js"; /** * This short name is used in the default basic queries an can be overwritten / used in * other queries * * @param {import("../../generated/common/types").CodeGenContext} context */ export function addShortNamesToQueryEnabledObjects(context) { const knownShortNames = {}; for (const type of getQueryEnabledObjects(context)) { if (!type.shortName) { type.shortName = type.name .split(/(?=[A-Z])/) .map((it) => (it[0] || "").toLowerCase()) .join(""); } if (knownShortNames[type.shortName]) { context.errors.push({ errorString: `Short name '${type.shortName}' is used by both '${ type.name }' and '${knownShortNames[type.shortName]}'. These short name values should be unique. Please call '.shortName()' on one or both of these types to set a custom value.`, }); } else { knownShortNames[type.shortName] = type.name; } // Also format schema if specified type.queryOptions.schema = type.queryOptions.schema ?? ""; if (type.queryOptions?.schema?.length > 0) { if (!type.queryOptions.schema.startsWith(`"`)) { type.queryOptions.schema = `"${type.queryOptions.schema}".`; } } } } /** * @param {import("../../generated/common/types").CodeGenContext} context * @returns {CodeGenObjectType[]} */ export function getQueryEnabledObjects(context) { return structureIteratorNamedTypes(context.structure).filter( (it) => it.type === "object" && "enableQueries" in it && it.enableQueries, ); } /** * Get primary key of object type. * If not exists, throw nicely. * The returned value is a copy, and not primary anymore. * * @param {import("../../generated/common/types").CodeGenContext} context * @param {CodeGenObjectType} type */ export function staticCheckPrimaryKey(context, type) { const entry = Object.entries(type.keys).find( (it) => it[1].sql?.primary || it[1].reference?.sql?.primary, ); if (isNil(entry)) { context.errors.push({ errorString: `Type '${type.name}' is missing a primary key. Either remove 'withPrimaryKey' from the options passed to 'enableQueries()' or add 'T.uuid().primary()' / 'T.number().primary()' to your type.`, }); } } /** * Get primary key of object type. * The returned value is a copy, and not primary anymore. * * @param {CodeGenObjectType} type * @returns {{ key: string, field: CodeGenType }} */ export function getPrimaryKeyWithType(type) { const entry = Object.entries(type.keys).find( (it) => it[1].sql?.primary || it[1].reference?.sql?.primary, ); return { key: entry[0], field: entry[1].reference ?? entry[1], }; } /** * Returns a sorted list of key names for the provided object type * - Primary keys * - Non nullable fields * - Nullable fields * - createdAt, updatedAt, deletedAt * * @param {CodeGenObjectType} type * @returns {string[]} */ export function getSortedKeysForType(type) { if (type?.internalSettings?._sortedKeys) { return type.internalSettings._sortedKeys; } const typeOrder = { boolean: 0, number: 1, uuid: 2, string: 3, date: 4, }; const result = /** @type {string[]} */ Object.keys(type.keys) .filter( (it) => it !== "createdAt" && it !== "updatedAt" && it !== "deletedAt", ) .sort((a, b) => { const fieldA = type.keys[a]?.reference ?? type.keys[a]; const fieldB = type.keys[b]?.reference ?? type.keys[b]; if (fieldA.sql?.primary) { return -1; } if (fieldB.sql?.primary) { return 1; } if ( fieldA.isOptional && isNil(fieldA.defaultValue) && !fieldB.isOptional ) { return 1; } else if ( !fieldA.isOptional && fieldB.isOptional && isNil(fieldB.defaultValue) ) { return -1; } const typeAIndex = typeOrder[fieldA.type] ?? 9; const typeBIndex = typeOrder[fieldB.type] ?? 9; if (typeAIndex !== typeBIndex) { return typeAIndex - typeBIndex; } return a.localeCompare(b); }); if (type.keys["createdAt"]) { result.push("createdAt"); } if (type.keys["updatedAt"]) { result.push("updatedAt"); } if (type.keys["deletedAt"]) { result.push("deletedAt"); } if (isNil(type.internalSettings)) { type.internalSettings = {}; } type.internalSettings._sortedKeys = result; return result; } /** * Statically check if objects are correctly setup do have queries enabled. * * @param {import("../../generated/common/types").CodeGenContext} context */ export function doSqlChecks(context) { const referencedKeySet = new Map(); for (const type of getQueryEnabledObjects(context)) { // Throw errors for missing primary keys staticCheckPrimaryKey(context, type); checkReservedObjectKeys(context, type); const ownKeySet = new Set(); for (const relation of type.relations) { if (!referencedKeySet.has(relation.reference.reference.uniqueName)) { referencedKeySet.set( relation.reference.reference.uniqueName, new Set(), ); } if (ownKeySet.has(relation.ownKey)) { context.errors.push({ errorString: `Type '${type.uniqueName}' has multiple relations with the same own key '${relation.ownKey}'. Please use unique own keys.`, }); } if (["manyToOne", "oneToOne"].includes(relation.subType)) { if ( referencedKeySet .get(relation.reference.reference.uniqueName) .has(relation.referencedKey) ) { context.errors.push({ errorString: `There are multiple relations to '${relation.reference.reference.uniqueName}'.'${relation.referencedKey}'. Make sure that they all have their own unique referenced key.`, }); } } ownKeySet.add(relation.ownKey); referencedKeySet .get(relation.reference.reference.uniqueName) .add(relation.referencedKey); staticCheckRelation(context, type, relation); } } } /** * Check if referenced side has enabled queries * * @param {import("../../generated/common/types").CodeGenContext} context * @param {CodeGenObjectType} type * @param {CodeGenRelationType} relation */ function staticCheckRelation(context, type, relation) { // Throw errors for missing enableQueries statements if (!relation.reference.reference.enableQueries) { const { name } = relation.reference.reference; context.errors.push({ errorString: `Type '${name}' did not call 'enableQueries' but '${type.name}' has a relation to it. Call 'enableQueries()' on '${name}' or remove the relation from '${type.name}'.`, }); } if (relation.subType === "manyToOne") { let found = false; for (const otherSide of relation.reference.reference.relations) { if ( otherSide.subType === "oneToMany" && relation.referencedKey === otherSide.ownKey ) { otherSide.referencedKey = relation.ownKey; found = true; break; } } if (!found) { const { name } = relation.reference.reference; context.errors.push({ errorString: `Relation from '${type.name}' is missing the inverse 'T.oneToMany()' on '${name}'. Add 'T.oneToMany("${relation.referencedKey}", T.reference("${type.group}", "${type.name}"))' to the 'relations()' call on '${name}'.`, }); } } else if (relation.subType === "oneToMany") { let found = false; for (const otherSide of relation.reference.reference.relations) { if ( otherSide.subType === "manyToOne" && relation.ownKey === otherSide.referencedKey ) { found = true; break; } } if (!found) { const { name } = relation.reference.reference; context.errors.push({ errorString: `Relation defined for '${type.name}', referencing '${name}' via '${relation.ownKey}' is unnecessary. Remove it or add the corresponding 'T.manyToOne()' call to '${name}'.`, }); } } checkReservedRelationNames(context, type, relation); if (relation.subType === "oneToOne") { createOneToOneReverseRelation(context, type, relation); } } /** * Create the reverse side of a one to one relation * * @param {import("../../generated/common/types").CodeGenContext} context * @param {CodeGenObjectType} type * @param {CodeGenRelationType} relation */ function createOneToOneReverseRelation(context, type, relation) { const inverseSide = relation.reference.reference; inverseSide.relations.push({ type: "relation", subType: "oneToOneReverse", ownKey: relation.referencedKey, referencedKey: relation.ownKey, reference: { type: "reference", isOptional: true, reference: type, }, }); checkReservedRelationNames( context, inverseSide, inverseSide.relations[inverseSide.relations.length - 1], ); } /** * Check if the object (recursively) uses any reserved keys like those used for atomic * updates * * @param {import("../../generated/common/types").CodeGenContext} context * @param {CodeGenObjectType} type */ function checkReservedObjectKeys(context, type) { const reservedKeys = Object.values(atomicUpdateFieldsTable).flat(); traverseType(context.structure, type, (objectType) => { if (objectType.type !== "object") { return; } for (const key of Object.keys(objectType.keys)) { if (reservedKeys.includes(key)) { context.errors.push({ errorString: `Type '${ objectType.uniqueName ?? type.uniqueName }' recursively uses the reserved key '${key}'. Use '${key.substring(1)}' instead.`, }); } } }); } /** * Check if relation keys use a reserved keyword. * Reserved keywords mainly keys used in the query builder * * @param {import("../../generated/common/types").CodeGenContext} context * @param {CodeGenObjectType} type * @param {CodeGenRelationType} relation */ function checkReservedRelationNames(context, type, relation) { const reservedRelationNames = [ "as", "limit", "offset", "orderBy", "orderBySpec", "select", "where", ]; if (reservedRelationNames.includes(relation.ownKey)) { context.errors.push({ errorString: `Relation name '${relation.ownKey}' from type '${type.name}' is a reserved keyword. Use another relation name.`, }); } }