UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

360 lines (314 loc) 11.2 kB
// @ts-nocheck import { isNil } from "@compas/stdlib"; import { AnyType } from "../../builders/AnyType.js"; import { AnyOfType, ArrayType, BooleanType, NumberType, ObjectType, } from "../../builders/index.js"; import { ReferenceType } from "../../builders/ReferenceType.js"; import { structureAddType } from "../../structure/structureAddType.js"; import { upperCaseFirst } from "../../utils.js"; import { js } from "../tag/index.js"; import { getTypeNameForType } from "../types.js"; import { getPrimaryKeyWithType, getQueryEnabledObjects, getSortedKeysForType, } from "./utils.js"; export const whereTypeTable = { number: ["equal", "notEqual", "in", "notIn", "greaterThan", "lowerThan"], date: ["equal", "notEqual", "in", "notIn", "greaterThan", "lowerThan"], uuid: ["equal", "notEqual", "in", "notIn"], string: ["equal", "notEqual", "in", "notIn", "like", "iLike", "notLike"], boolean: ["equal"], }; /** * Creates a where type and assigns in to the object type * * @param {import("../../generated/common/types").CodeGenContext} context */ export function createWhereTypes(context) { const defaults = { name: undefined, group: undefined, uniqueName: undefined, isOptional: true, defaultValue: undefined, }; for (const type of getQueryEnabledObjects(context)) { const fields = getSearchableFields(type); const fieldsArray = []; const whereType = new ObjectType(type.group, `${type.name}Where`).build(); whereType.uniqueName = `${upperCaseFirst(whereType.group)}${upperCaseFirst( whereType.name, )}`; whereType.keys["$raw"] = { ...new AnyType().optional().build(), rawValue: "QueryPart<any>", rawValueImport: { javaScript: undefined, typeScript: `import { QueryPart } from "@compas/store";`, }, rawValidator: "isQueryPart", rawValidatorImport: { javaScript: `import { isQueryPart } from "@compas/store";`, typeScript: `import { isQueryPart } from "@compas/store";`, }, }; whereType.keys["$or"] = { ...new ArrayType().values(true).optional().build(), values: new ReferenceType(type.group, `${type.name}Where`).build(), }; whereType.keys["$or"].values.reference = whereType; for (const key of Object.keys(fields)) { const fieldType = fields[key]; if (isNil(whereTypeTable[fieldType.type])) { continue; } const whereVariants = [...whereTypeTable[fieldType.type]]; if (type.queryOptions.withSoftDeletes && key === "deletedAt") { // Special case for deletedAt. // We want to be safe by default and thus not return any soft deleted rows, // without explicit consent whereVariants.push("includeNotNull"); } else if (fieldType.isOptional) { whereVariants.push("isNull", "isNotNull"); } for (const variant of whereVariants) { const name = variant === "equal" ? key : `${key}${upperCaseFirst(variant)}`; fieldsArray.push({ key, name, variant, }); if (["in", "notIn"].indexOf(variant) !== -1) { // Accept an array, instead of the plain type // Uses 'true' as a temporary placeholder to get the correct structure whereType.keys[name] = { ...new AnyOfType().values(true).optional().build(), values: [ { ...new ArrayType().values(true).optional().build(), values: { ...fieldType, ...defaults, isOptional: false }, }, { ...new AnyType().optional().build(), rawValue: "QueryPart<any>", rawValueImport: { javaScript: undefined, typeScript: `import { QueryPart } from "@compas/store";`, }, rawValidator: "isQueryPart", rawValidatorImport: { javaScript: `import { isQueryPart } from "@compas/store";`, typeScript: `import { isQueryPart } from "@compas/store";`, }, }, ], }; } else if ( ["isNull", "isNotNull", "includeNotNull"].indexOf(variant) !== -1 ) { // Accept a boolean instead of the plain type whereType.keys[name] = new BooleanType().optional().build(); } else { whereType.keys[name] = { ...fieldType, ...defaults }; } if (fieldType.sql?.primary && fieldType.type === "number") { // Get's a JS string to support 'bigserial', but just safely convert it to a // number. whereType.keys[name].validator.convert = true; } } } structureAddType(context.structure, whereType); type.where = { type: "", rawType: whereType, fields: fieldsArray, }; } // Add where, based on relations for (const type of getQueryEnabledObjects(context)) { for (const relation of type.relations) { const otherSide = relation.reference.reference; // Add via support via the where builder. type.where.rawType.keys[`via${upperCaseFirst(relation.ownKey)}`] = new ObjectType() .keys({ where: new ReferenceType( otherSide.group, `${otherSide.name}Where`, ).optional(), limit: new NumberType().optional(), offset: new NumberType().optional(), }) .optional() .build(); type.where.rawType.keys[ `via${upperCaseFirst(relation.ownKey)}` ].keys.where.reference = context.structure[otherSide.group][`${otherSide.name}Where`]; type.where.fields.push({ key: relation.ownKey, name: `via${upperCaseFirst(relation.ownKey)}`, variant: "via", isRelation: true, }); if ( relation.subType === "oneToMany" || relation.subType === "oneToOneReverse" ) { type.where.rawType.keys[`${relation.ownKey}NotExists`] = { ...new ReferenceType(otherSide.group, `${otherSide.name}Where`) .optional() .build(), reference: context.structure[otherSide.group][`${otherSide.name}Where`], }; type.where.fields.push({ key: relation.ownKey, name: `${relation.ownKey}NotExists`, variant: "notExists", isRelation: true, }); } } } // Add types to the system for (const type of getQueryEnabledObjects(context)) { type.where.type = getTypeNameForType(context, type.where.rawType, "", { useDefaults: false, }); } } /** * * @param {import("../../generated/common/types").CodeGenContext} context * @param {ImportCreator} imports * @param {CodeGenObjectType} type */ export function getWherePartial(context, imports, type) { let entityWhereString = `export const ${type.name}WhereSpec ={ "fieldSpecification": [`; const fieldsByKey = {}; for (const field of type.where.fields) { if (isNil(fieldsByKey[field.key])) { fieldsByKey[field.key] = []; } fieldsByKey[field.key].push(field); } for (const key of Object.keys(fieldsByKey)) { let matchers = `[`; let keyType = undefined; for (const field of fieldsByKey[key]) { if (isNil(keyType) && !field.isRelation) { const realField = type.keys[field.key].reference ?? type.keys[field.key]; // Type to cast arrays to, use for in & notIn keyType = realField.type === "number" && !realField.floatingPoint ? "int" : realField.type === "number" && realField.floatingPoint ? "float" : realField.type === "string" ? "varchar" : realField.type === "date" ? "timestamptz" : "uuid"; } matchers += `{ matcherKey: "${field.name}", matcherType: "${field.variant}", `; if (field.isRelation) { const relation = type.relations.find((it) => it.ownKey === field.key); const otherSide = relation.reference.reference; const isSelfReference = otherSide.name === type.name; const shortName = isSelfReference ? `${otherSide.shortName}2` : `${otherSide.shortName}`; if (!isSelfReference) { imports.destructureImport( `${otherSide.name}WhereSpec`, `./${otherSide.name}.js`, ); } const { key: primaryKey } = getPrimaryKeyWithType(type); const referencedKey = ["oneToMany", "oneToOneReverse"].indexOf(relation.subType) !== -1 ? relation.referencedKey : getPrimaryKeyWithType(otherSide).key; const ownKey = ["manyToOne", "oneToOne"].indexOf(relation.subType) !== -1 ? relation.ownKey : primaryKey; matchers += `relation: { entityName: "${otherSide.name}", shortName: "${shortName}", entityKey: "${referencedKey}", referencedKey: "${ownKey}", where: () => ${otherSide.name}WhereSpec, },`; } matchers += "},"; } matchers += "]"; entityWhereString += `{ tableKey: "${key}", keyType: "${keyType}", matchers: ${matchers} },`; } entityWhereString += " ]};"; return js` /** @type {any} */ ${entityWhereString} /** * Build 'WHERE ' part for ${type.name} * * @param {${type.where.type}} [where={}] * @param {string} [tableName="${type.shortName}."] * @param {{ skipValidator?: boolean|undefined }} [options={}] * @returns {QueryPart} */ export function ${type.name}Where(where = {}, tableName = "${type.shortName}.", options = {} ) { if (tableName.length > 0 && !tableName.endsWith(".")) { tableName = \`$\{tableName}.\`; } if (!options.skipValidator) { const whereValidated = validate${type.uniqueName}Where( where, "$.${type.name}Where"); if (whereValidated.error) { throw whereValidated.error; } where = whereValidated.value; } return generatedWhereBuilderHelper(${type.name}WhereSpec, where, tableName) } `; } /** * Returns an object with only the searchable fields * * @param {CodeGenObjectType} type * @returns {Record<string, CodeGenType>} */ export function getSearchableFields(type) { if (type?.internalSettings?._searchableFields) { return type.internalSettings._searchableFields; } const result = /** @type {Record<string, CodeGenType>} */ getSortedKeysForType(type) .map((it) => [it, type.keys[it]]) .filter((it) => it[1].sql?.searchable || it[1].reference?.sql?.searchable) .reduce((acc, [key, value]) => { // @ts-ignore acc[key] = value.reference ?? value; return acc; }, {}); if (!type.internalSettings) { type.internalSettings = {}; } type.internalSettings._searchableFields = result; return result; }