UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

480 lines (431 loc) 14.6 kB
import { AnyOfType, AnyType, ArrayType, NumberType, ObjectType, ReferenceType, } from "../builders/index.js"; import { databaseIsEnabled } from "../database/generator.js"; import { fileWriteRaw } from "../file/write.js"; import { typesTypescriptResolveFile } from "../types/typescript.js"; import { upperCaseFirst } from "../utils.js"; import { modelRelationGetInformation, modelRelationGetInverse, modelRelationGetOwn, } from "./model-relation.js"; import { structureModels } from "./models.js"; import { referenceUtilsGetProperty } from "./reference-utils.js"; import { structureAddType } from "./structure.js"; /** * Build the 'queryXxx' input types for all models. * * @param {import("../generate.js").GenerateContext} generateContext * @returns {void} */ export function modelQueryBuilderTypes(generateContext) { for (const model of structureModels(generateContext)) { const type = new ObjectType(model.group, `${model.name}QueryBuilder`) .keys({ where: new ReferenceType(model.group, `${model.name}Where`).optional(), orderBy: new ReferenceType( model.group, `${model.name}OrderBy`, ).optional(), orderBySpec: new ReferenceType( model.group, `${model.name}OrderBySpec`, ).optional(), limit: new NumberType().min(1).optional(), offset: new NumberType().min(0).optional(), select: new ReferenceType( model.group, `${model.name}Returning`, ).default(JSON.stringify(Object.keys(model.keys))), }) .build(); for (const relation of modelRelationGetOwn(model)) { const relationInfo = modelRelationGetInformation(relation); type.keys[relationInfo.keyNameOwn] = new ReferenceType( // @ts-expect-error relationInfo.modelInverse.group, `${relationInfo.modelInverse.name}QueryBuilder`, ) .optional() .build(); } for (const relation of modelRelationGetInverse(model)) { const relationInfo = modelRelationGetInformation(relation); type.keys[relationInfo.virtualKeyNameInverse] = new ReferenceType( // @ts-expect-error relationInfo.modelOwn.group, `${relationInfo.modelOwn.name}QueryBuilder`, ) .optional() .build(); } structureAddType(generateContext.structure, type, { skipReferenceExtraction: true, }); } } /** * Build the 'xxxQueryResult' output types for all models. * * @param {import("../generate.js").GenerateContext} generateContext * @returns {void} */ export function modelQueryResultTypes(generateContext) { for (const model of structureModels(generateContext)) { const expansionType = new ObjectType( "queryExpansion", model.group + upperCaseFirst(model.name), ) .keys({}) .build(); const type = new ObjectType( "queryResult", model.group + upperCaseFirst(model.name), ) .keys({}) .build(); type.keys = { ...model.keys, }; for (const relation of modelRelationGetOwn(model)) { const relationInfo = modelRelationGetInformation(relation); const existingType = type.keys[relationInfo.keyNameOwn]; const isOptional = referenceUtilsGetProperty( generateContext, type.keys[relationInfo.keyNameOwn], ["isOptional"], ); const joinedType = new ReferenceType( "queryResult", `${relationInfo.modelInverse.group}${upperCaseFirst( relationInfo.modelInverse.name, )}`, ).build(); const anyOfType = new AnyOfType().values(true); if (isOptional) { anyOfType.optional(); } type.keys[relationInfo.keyNameOwn] = anyOfType.build(); type.keys[relationInfo.keyNameOwn].values = [existingType, joinedType]; const joinedExpansionType = getQueryDefinitionReference( relationInfo.modelInverse.group, relationInfo.modelInverse.name, ); if (isOptional) { joinedExpansionType.optional(); } expansionType.keys[relationInfo.keyNameOwn] = joinedExpansionType.build(); } for (const relation of modelRelationGetInverse(model)) { const relationInfo = modelRelationGetInformation(relation); const joinedType = relation.subType === "oneToMany" ? new ArrayType().values( new ReferenceType( "queryResult", `${relationInfo.modelOwn.group}${upperCaseFirst( relationInfo.modelOwn.name, )}`, ), ) : new ReferenceType( "queryResult", `${relationInfo.modelOwn.group}${upperCaseFirst(relationInfo.modelOwn.name)}`, ); type.keys[relationInfo.virtualKeyNameInverse] = joinedType .optional() .build(); const joinedExpansionType = relation.subType === "oneToMany" ? new ArrayType().values( getQueryDefinitionReference( relationInfo.modelOwn.group, relationInfo.modelOwn.name, ), ) : getQueryDefinitionReference( relationInfo.modelOwn.group, relationInfo.modelOwn.name, ); if (relation.subType === "oneToOneReverse" && relation.isOptional) { // oneToMany's are never optional, since they then return an empty array. A oneToOne is // only optional if the owning side says so. joinedExpansionType.optional(); } expansionType.keys[relationInfo.virtualKeyNameInverse] = joinedExpansionType.build(); } structureAddType(generateContext.structure, type, { skipReferenceExtraction: true, }); structureAddType(generateContext.structure, expansionType, { skipReferenceExtraction: true, }); } } function getQueryDefinitionReference(group, name) { const resolvedName = `${upperCaseFirst(group)}${upperCaseFirst(name)}`; const implementation = { validatorInputType: `QueryDefinition${resolvedName}`, validatorOutputType: `QueryDefinition${resolvedName}`, }; return new AnyType().implementations({ js: implementation, ts: implementation, }); } /** * Add raw types related to models and query builders * * @param {import("../generate.js").GenerateContext} generateContext * @returns {void} */ export function modelQueryRawTypes(generateContext) { if (!databaseIsEnabled(generateContext)) { return; } const file = typesTypescriptResolveFile(generateContext); if (generateContext.options.targetLanguage === "ts") { fileWriteRaw( file, ` /// ============================ /// Query builder resolver types /** * Utility type to resolve the full type instead of showing things like \`Omit<Type, * SomeNesting<...>> & ...\` in the type popups and errors. */ type _ResolveType<T> = { [K in keyof T]: T[K] } & {}; /** * Utility type to resolve the base + expansion of an entity. */ interface QueryBuilderDefinition<Base, Expansion> { base: Base; expansion: Expansion; } /** * Omit never values */ type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K]; }; /** * Apply Partial if the type can be undefined. */ type ConvertUndefinedToPartial<T> = { [K in keyof T as undefined extends T[K] ? K : never]?: T[K]; } & { [K in keyof T as undefined extends T[K] ? never : K]: T[K]; }; type QueryBuilderSpecialKeys = | "offset" | "limit" | "orderBy" | "orderBySpec" | "select" | "where"; /** * Max value for which optional joins are resolved. */ type ResolveJoinDepth = 3; type InferExpansionForOptionalJoins<Expansion> = Exclude<Expansion extends { expansion: infer Result; } ? Result : Expansion extends Array<{ expansion: infer Result }> ? Result : Expansion, undefined>; /** * Provided an QueryBuilder expansion object, determines the union of all possible joins up * until {@link ResolveJoinDepth} depth. */ export type ResolveOptionalJoins< Expansion, Prefix extends string = "", Depth extends Array<unknown> = [], ResolvedExpansion = InferExpansionForOptionalJoins<Expansion>, > = Depth["length"] extends ResolveJoinDepth ? never : { [K in keyof ResolvedExpansion]: K extends string ? Prefix extends "" // Base case ? | \`$\{K}\` | ResolveOptionalJoins< ResolvedExpansion[K], \`$\{K}\`, [unknown, ...Depth] > : // Nested case | \`$\{Prefix}.$\{K}\` // Recursive into other expansions. | ResolveOptionalJoins< ResolvedExpansion[K], \`$\{Prefix}.$\{K}\`, [unknown, ...Depth] > : never; }[keyof ResolvedExpansion]; /** * Split the input string on the first '.'-char. */ type SplitDot<Input extends string> = Input extends \`$\{infer Start}.$\{string}\` ? Start : never; /** * Check if the provided key is in one of the optional joins. This is also true when the key * is a prefix of a join. i.e \`settings\` is optional if \`settings.user\` is an optional join. */ type IsOptionalJoin< Key extends string, Joins extends string, > = Key extends Joins ? true : Key extends SplitDot<Joins> ? true : false; /** * Filters and strips the Joins that start with Prefix. */ type FilterOptionalJoins<Joins extends string, Prefix extends string> = { [K in Joins]: K extends \`$\{Prefix}\` ? never : K extends \`$\{Prefix}.$\{infer Suffix}\` ? Suffix : never; }[Joins]; /** * Pick the selected fields from the Type. * If no select field exists on the builder, or if "*" is supplied, the full Type is returned. */ type PickSelected<Type, SelectBuilder> = SelectBuilder extends { select: "*" | Array<string>; } ? SelectBuilder["select"] extends "*" // Select all fields ? Type : SelectBuilder["select"] extends Array<infer K extends keyof Type> // Only select the fields ? // that have been selected. Pick<Type, K> : never // Defaults to selecting all fields. : Type; type ResolveBaseResult< Base, QueryBuilder, OptionalJoins extends string, > = OmitNever< ConvertUndefinedToPartial< PickSelected< Omit< Base, (keyof QueryBuilder) | OptionalJoins >, QueryBuilder > > >; type ResolveTypeFromExpansion< DefinitionType, QueryBuilder, OptionalJoins extends string, > = DefinitionType extends Array<infer SingleDefinition> ? Array<QueryBuilderResolver<SingleDefinition, QueryBuilder, OptionalJoins>> : QueryBuilderResolver<DefinitionType, QueryBuilder, OptionalJoins>; type ResolveExpansionKey< K extends keyof Expansion & string, Base, Expansion, QueryBuilder, OptionalJoins extends string, > = K extends keyof QueryBuilder ? ResolveTypeFromExpansion< Expansion[K], QueryBuilder[K], FilterOptionalJoins<OptionalJoins, K> > : IsOptionalJoin<K, OptionalJoins> extends true ? K extends keyof Base ? // We need to include the base type if it exists for owning sides of relations. | ResolveTypeFromExpansion< Expansion[K], unknown, FilterOptionalJoins<OptionalJoins, K> > | Base[K] | undefined : | ResolveTypeFromExpansion< Expansion[K], unknown, FilterOptionalJoins<OptionalJoins, K> > | undefined : K extends keyof Base ? Base[K] : never; /** * Provided a Definition and a QueryBuilder, resolves the return type. * * For usage of this type in function parameter definitions, OptionalJoins can be supplied. */ export type QueryBuilderResolver< DefinitionType, QueryBuilder, OptionalJoins extends string = "", > = DefinitionType extends QueryBuilderDefinition<infer Base, infer Expansion> ? _ResolveType< ResolveBaseResult<Base, QueryBuilder, OptionalJoins> & OmitNever< ConvertUndefinedToPartial< _ResolveType<{ [K in Exclude< Exclude<keyof Expansion, QueryBuilderSpecialKeys>, number | symbol >]: ResolveExpansionKey< K, Base, Expansion, QueryBuilder, OptionalJoins >; }> > > > : DefinitionType extends undefined ? undefined : never; /// End Query builder resolver types /// ================================ `, ); } const exportPrefix = generateContext.options.generators.types?.declareGlobalTypes ? "" : "export"; for (const model of structureModels(generateContext)) { const name = `${upperCaseFirst(model.group)}${upperCaseFirst(model.name)}`; if (generateContext.options.targetLanguage === "ts") { fileWriteRaw( file, `${exportPrefix} interface QueryDefinition${name} { base: ${name}; expansion: QueryExpansion${name}; }\n`, ); fileWriteRaw( file, `${exportPrefix} type ${name}OptionalJoins = ResolveOptionalJoins<QueryExpansion${name}>;\n`, ); fileWriteRaw( file, `${exportPrefix} type ${name}QueryResolver<QueryBuilder extends ${name}QueryBuilder, const OptionalJoins extends ${name}OptionalJoins = never> = QueryBuilderResolver<QueryDefinition${name}, QueryBuilder, OptionalJoins>;\n\n`, ); } else if (generateContext.options.targetLanguage === "js") { fileWriteRaw( file, `${exportPrefix} type QueryDefinition${name} = import("@compas/store").QueryBuilderDefinition<${name}, QueryExpansion${name}>;\n`, ); fileWriteRaw( file, `${exportPrefix} type ${name}QueryResolver<QueryBuilder extends ${name}QueryBuilder, const OptionalJoins extends import("@compas/store").ResolveOptionalJoins<QueryExpansion${name}> = never> = import("@compas/store").QueryBuilderResolver<QueryDefinition${name}, QueryBuilder, OptionalJoins>;\n\n`, ); } } }