diff --git a/packages/cli/src/generated/common/types.d.ts b/packages/cli/src/generated/common/types.d.ts index 903166779a..d30f54f517 100644 --- a/packages/cli/src/generated/common/types.d.ts +++ b/packages/cli/src/generated/common/types.d.ts @@ -1,6 +1,7 @@ // Generated by @compas/code-gen + export type CliFlagDefinition = { "name": string; "rawName": string; @@ -94,5 +95,3 @@ export type CliCompletion = "specification": "boolean"|"number"|"string"|"booleanOrString"; "description"?: string|undefined; }; - - diff --git a/packages/code-gen/package.json b/packages/code-gen/package.json index 6375ece44c..e5dbb362b3 100644 --- a/packages/code-gen/package.json +++ b/packages/code-gen/package.json @@ -12,13 +12,7 @@ "./package.json": "./package.json" }, "type": "module", - "keywords": [ - "compas", - "generate", - "router", - "validation", - "code-gen" - ], + "keywords": ["compas", "generate", "router", "validation", "code-gen"], "license": "MIT", "dependencies": { "@compas/stdlib": "0.15.2" diff --git a/packages/code-gen/src/database/ts-postgres.js b/packages/code-gen/src/database/ts-postgres.js index 03404457f9..4ace6f49d0 100644 --- a/packages/code-gen/src/database/ts-postgres.js +++ b/packages/code-gen/src/database/ts-postgres.js @@ -56,6 +56,7 @@ export function tsPostgresGenerateUtils(generateContext) { helperImportCollector.destructure("@compas/stdlib", "AppError"); helperTypeImportCollector.destructure("@compas/store", "QueryPart"); helperTypeImportCollector.destructure("@compas/store", "WrappedQueryPart"); + helperTypeImportCollector.destructure("@compas/store", "WrappedQueryResult"); fileWrite( helperFile, @@ -156,7 +157,10 @@ export function tsPostgresCreateFile(generateContext, model) { importCollector.destructure("@compas/stdlib", "isNil"); importCollector.destructure("@compas/stdlib", "AppError"); + typeImportCollector.destructure("@compas/store", "Postgres"); + typeImportCollector.destructure("@compas/store", "QueryPart"); typeImportCollector.destructure("@compas/store", "WrappedQueryPart"); + typeImportCollector.destructure("@compas/store", "WrappedQueryResult"); fileWrite(file, `\nexport const ${model.name}Queries = {`); fileContextSetIndent(file, 1); @@ -1119,6 +1123,9 @@ export function tsPostgresGenerateQueryBuilder( entityInformation: `$$(): any => ${relationInfo.modelInverse.name}QueryBuilderSpec$$`, }); } + + const fullTypeName = `${upperCaseFirst(model.group)}${upperCaseFirst(model.name)}`; + for (const relation of inverseRelations) { const relationInfo = modelRelationGetInformation(relation); @@ -1165,7 +1172,7 @@ export function tsPostgresGenerateQueryBuilder( // Function fileBlockStart( file, - `export function query${upperCaseFirst(model.name)}(input: ${contextNames.queryBuilderType.inputType} = {}): WrappedQueryPart<${contextNames.queryResultType.outputType}>`, + `export function query${upperCaseFirst(model.name)}(input: QueryBuilder = {}): WrappedQueryResult<${fullTypeName}QueryResolver>`, ); // Input validation diff --git a/packages/code-gen/src/generate.js b/packages/code-gen/src/generate.js index a77f5c3b6e..7458adbf55 100644 --- a/packages/code-gen/src/generate.js +++ b/packages/code-gen/src/generate.js @@ -24,6 +24,7 @@ import { } from "./processors/model-partials.js"; import { modelQueryBuilderTypes, + modelQueryRawTypes, modelQueryResultTypes, } from "./processors/model-query.js"; import { @@ -153,6 +154,7 @@ export function generateExecute(generator, options) { typesGeneratorInit(generateContext); databaseGenerator(generateContext); + modelQueryRawTypes(generateContext); routerGenerator(generateContext); apiClientGenerator(generateContext); diff --git a/packages/code-gen/src/generated/common/types.d.ts b/packages/code-gen/src/generated/common/types.d.ts index dbcca48649..52c8f7d8a1 100644 --- a/packages/code-gen/src/generated/common/types.d.ts +++ b/packages/code-gen/src/generated/common/types.d.ts @@ -1,6 +1,7 @@ // Generated by @compas/code-gen + export type StructureAnyDefinitionTarget = "js"|"ts"|"jsKoaReceive"|"tsKoaReceive"|"jsKoaSend"|"tsKoaSend"|"jsPostgres"|"tsPostgres"|"jsAxios"|"tsAxios"|"jsAxiosNode"|"tsAxiosBrowser"|"tsAxiosReactNative"|"jsFetch"|"tsFetch"|"jsFetchNode"|"tsFetchBrowser"|"tsFetchReactNative"; export type StructureAnyDefinition = { @@ -1388,5 +1389,3 @@ export type StructureTypeDefinitionInput = |StructureReferenceDefinitionInput |StructureRelationDefinitionInput |StructureRouteInvalidationDefinitionInput; - - diff --git a/packages/code-gen/src/processors/model-query.js b/packages/code-gen/src/processors/model-query.js index e11f2fb50f..79cf00bff1 100644 --- a/packages/code-gen/src/processors/model-query.js +++ b/packages/code-gen/src/processors/model-query.js @@ -1,10 +1,15 @@ import { AnyOfType, + AnyType, ArrayType, NumberType, ObjectType, ReferenceType, } from "../builders/index.js"; +import { databaseIsEnabled } from "../database/generator.js"; +import { fileWriteRaw } from "../file/write.js"; +import { TypescriptImportCollector } from "../target/typescript.js"; +import { typesTypescriptResolveFile } from "../types/typescript.js"; import { upperCaseFirst } from "../utils.js"; import { modelRelationGetInformation, @@ -79,6 +84,13 @@ export function modelQueryBuilderTypes(generateContext) { */ 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), @@ -99,6 +111,7 @@ export function modelQueryResultTypes(generateContext) { type.keys[relationInfo.keyNameOwn], ["isOptional"], ); + const joinedType = new ReferenceType( "queryResult", `${relationInfo.modelInverse.group}${upperCaseFirst( @@ -110,9 +123,19 @@ export function modelQueryResultTypes(generateContext) { if (isOptional) { anyOfType.optional(); } - type.keys[relationInfo.keyNameOwn] = anyOfType.build(); + 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)) { @@ -136,10 +159,97 @@ export function modelQueryResultTypes(generateContext) { 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, + ); + + expansionType.keys[relationInfo.virtualKeyNameInverse] = + joinedExpansionType.optional().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, + jsPostgres: implementation, + tsPostgres: 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") { + const typeImports = TypescriptImportCollector.getImportCollector( + file, + true, + ); + typeImports.destructure("@compas/store", "QueryBuilderResolver"); + typeImports.destructure("@compas/store", "QueryBuilderDefinition"); + typeImports.destructure("@compas/store", "ResolveOptionalJoins"); + } + + 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} type QueryDefinition${name} = QueryBuilderDefinition<${name}, QueryExpansion${name}>;\n`, + ); + fileWriteRaw( + file, + `${exportPrefix} type ${name}QueryResolver = never> = QueryBuilderResolver;\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 = never> = import("@compas/store").QueryBuilderResolver;\n\n`, + ); + } } } diff --git a/packages/code-gen/src/types/typescript.js b/packages/code-gen/src/types/typescript.js index 814348736e..b600f9082c 100644 --- a/packages/code-gen/src/types/typescript.js +++ b/packages/code-gen/src/types/typescript.js @@ -45,6 +45,7 @@ export function typesTypescriptResolveFile(generateContext) { export function typesTypescriptInitFile(generateContext) { return fileContextCreateGeneric(generateContext, "common/types.d.ts", { importCollector: new TypescriptImportCollector(), + typeImportCollector: new TypescriptImportCollector(true), }); } diff --git a/packages/code-gen/src/validators/javascript.js b/packages/code-gen/src/validators/javascript.js index 0566b34176..04fe444dc2 100644 --- a/packages/code-gen/src/validators/javascript.js +++ b/packages/code-gen/src/validators/javascript.js @@ -121,7 +121,9 @@ export function validatorJavascriptGetFile(generateContext, type) { /** * @typedef {Record} ValidatorErrorMap */ + +// eslint-disable-next-line unused-imports/no-unused-vars const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v); `, ); diff --git a/packages/code-gen/src/validators/typescript.js b/packages/code-gen/src/validators/typescript.js index 606fad7d63..6fa3f1e380 100644 --- a/packages/code-gen/src/validators/typescript.js +++ b/packages/code-gen/src/validators/typescript.js @@ -62,6 +62,7 @@ type Either = { value: T; error?: never }|{ value?: never; error: E }; type ValidatorErrorMap = Record; +// eslint-disable-next-line unused-imports/no-unused-vars const isRecord = (v: unknown): v is Record => !!v && typeof v === "object" && !Array.isArray(v); `, ); diff --git a/packages/create-compas/package.json b/packages/create-compas/package.json index 71c2e61e70..e7a3d07b6a 100644 --- a/packages/create-compas/package.json +++ b/packages/create-compas/package.json @@ -9,10 +9,7 @@ "bin": { "create-compas": "src/create-compas.js" }, - "keywords": [ - "compas", - "create" - ], + "keywords": ["compas", "create"], "license": "MIT", "dependencies": { "@compas/stdlib": "0.15.2", diff --git a/packages/server/package.json b/packages/server/package.json index bd1e7f6a1a..44ebce3084 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -12,13 +12,7 @@ "./package.json": "./package.json" }, "type": "module", - "keywords": [ - "compas", - "koa", - "api", - "http", - "backend" - ], + "keywords": ["compas", "koa", "api", "http", "backend"], "license": "MIT", "dependencies": { "@compas/stdlib": "0.15.2", diff --git a/packages/stdlib/package.json b/packages/stdlib/package.json index 8a43e1bc73..0838583c4d 100644 --- a/packages/stdlib/package.json +++ b/packages/stdlib/package.json @@ -12,12 +12,7 @@ "./package.json": "./package.json" }, "type": "module", - "keywords": [ - "compas", - "stdlib", - "standard", - "logger" - ], + "keywords": ["compas", "stdlib", "standard", "logger"], "license": "MIT", "dependencies": { "@types/node": "22.9.0", diff --git a/packages/store/index.js b/packages/store/index.js index 75d6e5e7a4..8eea23065e 100644 --- a/packages/store/index.js +++ b/packages/store/index.js @@ -17,13 +17,30 @@ /** * @template Type - * @template {undefined | "*" | Array} Selector - * @typedef {import("./types/advanced-types.js").Returning} Returning + * @typedef {import("./types/advanced-types.js").WrappedQueryPart} WrappedQueryPart */ /** * @template Type - * @typedef {import("./types/advanced-types.js").WrappedQueryPart} WrappedQueryPart + * @typedef {import("./types/advanced-types.js").WrappedQueryResult} WrappedQueryResult + */ + +/** + * @template Base + * @template Expansion + * @typedef {import("./types/advanced-types.js").QueryBuilderDefinition} QueryBuilderDefinition + */ + +/** + * @template DefinitionType + * @template QueryBuilder + * @template {string} OptionalJoins + * @typedef {import("./types/advanced-types.js").QueryBuilderResolver} QueryBuilderResolver + */ + +/** + * @template Expansion + * @typedef {import("./types/advanced-types.js").ResolveOptionalJoins} ResolveOptionalJoins */ /** diff --git a/packages/store/package.json b/packages/store/package.json index 994d1b05b4..435c892e91 100644 --- a/packages/store/package.json +++ b/packages/store/package.json @@ -12,12 +12,7 @@ "./package.json": "./package.json" }, "type": "module", - "keywords": [ - "compas", - "s3", - "postgres", - "persistence" - ], + "keywords": ["compas", "s3", "postgres", "persistence"], "license": "MIT", "dependencies": { "@aws-sdk/client-s3": "3.685.0", diff --git a/packages/store/src/generated/common/types.d.ts b/packages/store/src/generated/common/types.d.ts index 4632765040..94ba9b3a3b 100644 --- a/packages/store/src/generated/common/types.d.ts +++ b/packages/store/src/generated/common/types.d.ts @@ -1,6 +1,7 @@ // Generated by @compas/code-gen + /** * User definable, optional object to store whatever you want */ @@ -1775,6 +1776,18 @@ export type StoreSessionStoreTokenUpdateValidated = { "returning"?: StoreSessionStoreTokenReturning|undefined; }; +export type QueryDefinitionStoreFile = import("@compas/store").QueryBuilderDefinition; +export type StoreFileQueryResolver = never> = import("@compas/store").QueryBuilderResolver; + +export type QueryDefinitionStoreJob = import("@compas/store").QueryBuilderDefinition; +export type StoreJobQueryResolver = never> = import("@compas/store").QueryBuilderResolver; + +export type QueryDefinitionStoreSessionStore = import("@compas/store").QueryBuilderDefinition; +export type StoreSessionStoreQueryResolver = never> = import("@compas/store").QueryBuilderResolver; + +export type QueryDefinitionStoreSessionStoreToken = import("@compas/store").QueryBuilderDefinition; +export type StoreSessionStoreTokenQueryResolver = never> = import("@compas/store").QueryBuilderResolver; + export type StoreFileResponse = { "id": string; "name": string; @@ -2477,4 +2490,18 @@ export type StoreSessionStoreQueryBuilderInput = { "accessTokens"?: StoreSessionStoreTokenQueryBuilderInput|undefined; }; +export type QueryExpansionStoreFile = { +}; +export type QueryExpansionStoreJob = { +}; + +export type QueryExpansionStoreSessionStore = { + "accessTokens"?: (QueryDefinitionStoreSessionStoreToken)[]|undefined; +}; + +export type QueryExpansionStoreSessionStoreToken = { + "refreshToken"?: QueryDefinitionStoreSessionStoreToken|undefined; + "session": QueryDefinitionStoreSessionStore; + "accessToken"?: QueryDefinitionStoreSessionStoreToken|undefined; +}; diff --git a/packages/store/src/generated/queryExpansion/validators.js b/packages/store/src/generated/queryExpansion/validators.js new file mode 100644 index 0000000000..583a1ae185 --- /dev/null +++ b/packages/store/src/generated/queryExpansion/validators.js @@ -0,0 +1,284 @@ +// Generated by @compas/code-gen + +/** + * @template T, E + * @typedef {{ value: T, error?: never}|{ value?: never, error: E }} Either + */ + +/** + * @typedef {Record} ValidatorErrorMap + */ + +const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v); + +/** + * @param {import("../common/types.js").QueryExpansionStoreFile|any} value + * @returns {Either} + */ +export function validateQueryExpansionStoreFile(value) { + /** @type {ValidatorErrorMap} */ + const errorMap = {}; + /** @type {any} */ + let result = undefined; + + if (value === null || value === undefined) { + errorMap[`$`] = { + key: "validator.undefined", + }; + } else { + if (!isRecord(value)) { + errorMap[`$`] = { + key: "validator.object", + value: value, + foundType: typeof value, + }; + } else { + /** @type {Set} */ + const knownKeys0 = new Set([]); + for (const key of Object.keys(value)) { + if ( + !knownKeys0.has(key) && + value[key] !== null && + value[key] !== undefined + ) { + const expectedKeys = [...knownKeys0]; + const foundKeys = Object.keys(value); + const unknownKeys = foundKeys.filter((it) => !knownKeys0.has(it)); + errorMap[`$`] = { + key: "validator.keys", + unknownKeys, + expectedKeys, + foundKeys, + }; + break; + } + } + result = {}; + } + } + if (Object.keys(errorMap).length > 0) { + return { error: errorMap }; + } + return { value: result }; +} + +/** + * @param {import("../common/types.js").QueryExpansionStoreJob|any} value + * @returns {Either} + */ +export function validateQueryExpansionStoreJob(value) { + /** @type {ValidatorErrorMap} */ + const errorMap = {}; + /** @type {any} */ + let result = undefined; + + if (value === null || value === undefined) { + errorMap[`$`] = { + key: "validator.undefined", + }; + } else { + if (!isRecord(value)) { + errorMap[`$`] = { + key: "validator.object", + value: value, + foundType: typeof value, + }; + } else { + /** @type {Set} */ + const knownKeys0 = new Set([]); + for (const key of Object.keys(value)) { + if ( + !knownKeys0.has(key) && + value[key] !== null && + value[key] !== undefined + ) { + const expectedKeys = [...knownKeys0]; + const foundKeys = Object.keys(value); + const unknownKeys = foundKeys.filter((it) => !knownKeys0.has(it)); + errorMap[`$`] = { + key: "validator.keys", + unknownKeys, + expectedKeys, + foundKeys, + }; + break; + } + } + result = {}; + } + } + if (Object.keys(errorMap).length > 0) { + return { error: errorMap }; + } + return { value: result }; +} + +/** + * @param {import("../common/types.js").QueryExpansionStoreSessionStore|any} value + * @returns {Either} + */ +export function validateQueryExpansionStoreSessionStore(value) { + /** @type {ValidatorErrorMap} */ + const errorMap = {}; + /** @type {any} */ + let result = undefined; + + if (value === null || value === undefined) { + errorMap[`$`] = { + key: "validator.undefined", + }; + } else { + if (!isRecord(value)) { + errorMap[`$`] = { + key: "validator.object", + value: value, + foundType: typeof value, + }; + } else { + /** @type {Set} */ + const knownKeys0 = new Set(["accessTokens"]); + for (const key of Object.keys(value)) { + if ( + !knownKeys0.has(key) && + value[key] !== null && + value[key] !== undefined + ) { + const expectedKeys = [...knownKeys0]; + const foundKeys = Object.keys(value); + const unknownKeys = foundKeys.filter((it) => !knownKeys0.has(it)); + errorMap[`$`] = { + key: "validator.keys", + unknownKeys, + expectedKeys, + foundKeys, + }; + break; + } + } + result = { accessTokens: undefined }; + + if ( + value["accessTokens"] === null || + value["accessTokens"] === undefined + ) { + result["accessTokens"] = undefined; + } else { + /** @type {ValidatorErrorMap} */ + const intermediateErrorMap2 = {}; + /** @type {Array} */ + const intermediateResult2 = []; + /** @type {any | Array} */ + const intermediateValue2 = value["accessTokens"]; + + if (!Array.isArray(intermediateValue2)) { + errorMap[`$.accessTokens`] = { + key: "validator.array", + value: intermediateValue2, + }; + } else { + result["accessTokens"] = []; + for (let i2 = 0; i2 < intermediateValue2.length; ++i2) { + if ( + intermediateValue2[i2] === null || + intermediateValue2[i2] === undefined + ) { + intermediateErrorMap2[`$.${i2}`] = { + key: "validator.undefined", + }; + } else { + intermediateResult2[i2] = intermediateValue2[i2]; + } + } + } + if (Object.keys(intermediateErrorMap2).length) { + for (const errorKey of Object.keys(intermediateErrorMap2)) { + errorMap[`$.accessTokens${errorKey.substring(1)}`] = + intermediateErrorMap2[errorKey]; + } + } else { + result["accessTokens"] = intermediateResult2; + } + } + } + } + if (Object.keys(errorMap).length > 0) { + return { error: errorMap }; + } + return { value: result }; +} + +/** + * @param {import("../common/types.js").QueryExpansionStoreSessionStoreToken|any} value + * @returns {Either} + */ +export function validateQueryExpansionStoreSessionStoreToken(value) { + /** @type {ValidatorErrorMap} */ + const errorMap = {}; + /** @type {any} */ + let result = undefined; + + if (value === null || value === undefined) { + errorMap[`$`] = { + key: "validator.undefined", + }; + } else { + if (!isRecord(value)) { + errorMap[`$`] = { + key: "validator.object", + value: value, + foundType: typeof value, + }; + } else { + /** @type {Set} */ + const knownKeys0 = new Set(["refreshToken", "session", "accessToken"]); + for (const key of Object.keys(value)) { + if ( + !knownKeys0.has(key) && + value[key] !== null && + value[key] !== undefined + ) { + const expectedKeys = [...knownKeys0]; + const foundKeys = Object.keys(value); + const unknownKeys = foundKeys.filter((it) => !knownKeys0.has(it)); + errorMap[`$`] = { + key: "validator.keys", + unknownKeys, + expectedKeys, + foundKeys, + }; + break; + } + } + result = { + refreshToken: undefined, + session: undefined, + accessToken: undefined, + }; + + if ( + value["refreshToken"] === null || + value["refreshToken"] === undefined + ) { + result["refreshToken"] = undefined; + } else { + result["refreshToken"] = value["refreshToken"]; + } + if (value["session"] === null || value["session"] === undefined) { + errorMap[`$.session`] = { + key: "validator.undefined", + }; + } else { + result["session"] = value["session"]; + } + if (value["accessToken"] === null || value["accessToken"] === undefined) { + result["accessToken"] = undefined; + } else { + result["accessToken"] = value["accessToken"]; + } + } + } + if (Object.keys(errorMap).length > 0) { + return { error: errorMap }; + } + return { value: result }; +} diff --git a/packages/store/types/advanced-types.d.ts b/packages/store/types/advanced-types.d.ts index e602f1049a..12f58ed244 100644 --- a/packages/store/types/advanced-types.d.ts +++ b/packages/store/types/advanced-types.d.ts @@ -24,7 +24,7 @@ export interface QueryPart { /** * Wrap query builder results */ -export type WrappedQueryPart = { +export type WrappedQueryPart = { /** * Get the underlying queryPart, representing the build query */ @@ -41,24 +41,215 @@ export type WrappedQueryPart = { exec(sql: Postgres): Promise; /** - * Exec the query and return raw query results. This should be used when a custom 'select' or 'returning' is used. + * Exec the query and return raw query results. This should be used when a custom 'select' + * or 'returning' is used. */ execRaw(sql: Postgres): Promise[]>; }; /** - * @template Type + * Wrap query builder results + */ +export type WrappedQueryResult = { + /** + * Get the underlying queryPart, representing the build query + */ + queryPart: QueryPart; + + /** + * @deprecated Use `.exec` or `.execRaw` + */ + then(): never; + + /** + * Exec the query and return validated query results. + */ + exec(sql: Postgres): Promise; + + /** + * Exec the query and return raw query results. This should be used when a custom 'select' + * or 'returning' is used. + */ + execRaw(sql: Postgres): Promise; +}; + + +// ------ QueryBuilder Type Resolver ------ + +/** + * Utility type to resolve the full type instead of showing things like `Omit> & ...` in the type popups and errors. + */ +type _ResolveType = { [K in keyof T]: T[K] } & {}; + +/** + * Utility type to resolve the base + expansion of an entity. + */ +export type QueryBuilderDefinition = { + base: Base; + expansion: Expansion; +}; + +type PickKeysThatExtend = { + [K in keyof T as T[K] extends Select ? K : never]: T[K]; +}; + +type OmitKeysThatExtend = { + [K in keyof T as T[K] extends Select ? never : K]: T[K]; +}; + +type ConvertNeverAndUndefined = + OmitKeysThatExtend, undefined> + & Partial>; + +type QueryBuilderSpecialKeys = + | "offset" + | "limit" + | "orderBy" + | "orderBySpec" + | "select" + | "where"; + +/** + * Max value for which optional joins are resolved. + */ +type ResolveJoinDepth = 6; + +/** + * 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 = [], +> = Depth["length"] extends ResolveJoinDepth + ? never + : Expansion extends object + ? { + [K in keyof Expansion]: K extends string + ? Expansion[K] extends { + expansion: unknown; + } + ? Prefix extends "" // Base case + ? + | `${K}` + | ResolveOptionalJoins< + Expansion[K]["expansion"], + `${K}`, + [unknown, ...Depth] + > + : // Nested case + | `${Prefix}.${K}` // Recursive into other expansions. + | ResolveOptionalJoins< + Expansion[K]["expansion"], + `${Prefix}.${K}`, + [unknown, ...Depth] + > + : never + : never; + }[keyof Expansion] + : never; + +/** + * Split the input string on the first '.'-char. + */ +type SplitDot = 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 ? true : false; + +/** + * Filters and strips the Joins that start with Prefix. + */ +type FilterOptionalJoins = { + [K in Joins]: K extends `${Prefix}` + ? never + : K extends `${Prefix}.${infer Suffix}` + ? Suffix + : never; +}[Joins]; - * @typedef {object} WrappedQueryPart - * @property {import("@compas/store").QueryPart} queryPart - * @property {function(): void} then - * @property {(sql: import("@compas/store").Postgres) => Promise} exec - * @property {(sql: import("@compas/store").Postgres) => Promise<(Type|any)[]>} execRaw +/** + * 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 = SelectBuilder extends { + select: "*" | Array; + } + ? SelectBuilder["select"] extends "*" // Select all fields + ? Type + : SelectBuilder["select"] extends Array + ? // Only select the fields that have been selected. + Pick + : never + : // Defaults to selecting all fields. + Type; + +type ResolveBaseResult = PickSelected< + Omit< + Base, + Exclude | OptionalJoins + >, + QueryBuilder +> + +type ResolveTypeFromExpansion = DefinitionType extends Array + ? Array> + : QueryBuilderResolver; -export type Returning = - Selector extends "*" ? Type[] - : Selector extends Array ? - // @ts-ignore - Pick[] - : undefined; +type ResolveExpansionKey< + K extends (keyof Expansion & string), + Base, Expansion, QueryBuilder, + OptionalJoins extends string +> = K extends keyof QueryBuilder + ? ResolveTypeFromExpansion< + Expansion[K], + QueryBuilder[K], + FilterOptionalJoins + > + : + IsOptionalJoin 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 + > | Base[K] | undefined + : + | ResolveTypeFromExpansion< + Expansion[K], + unknown, + FilterOptionalJoins + > + | 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 + ? _ResolveType & + ConvertNeverAndUndefined<_ResolveType<{ + [K in Exclude, number | symbol>]: _ResolveType> + }>>> + : never;