UNPKG

blade

Version:
242 lines (239 loc) • 9.58 kB
import { t as RootServerContext } from "../../context-BBeH3CVO.js"; import "../../remote-storage-CThzVgzV.js"; import "../../utils-BeDKhSpT.js"; import { r as QUERY_SYMBOLS } from "../../dist-C7NzDGd9.js"; import { n as getSyntaxProxy, t as getBatchProxy } from "../../queries-CO8sEzJ9.js"; import "../../serialize-error-CkyRja0C.js"; import "../../errors-lnR4nSY9.js"; import { t as deserializeError } from "../../errors-B8jB1JUF.js"; import "../../html-BHm7adlz.js"; import { n as generateHashSync } from "../../utils-FoLYJor_.js"; import { useContext } from "react"; //#region private/server/utils/pagination.ts /** * Allows for parsing the `?page` query string parameter used for pagination. * * @param queryParam - The value of the `?page` query string parameter. * * @returns The parsed information contained within the `?page` parameter. */ const parsePaginationQueryParam = (queryParam) => { if (!queryParam) return null; const [leafIndex, hookHash, queryIndex, direction, cursor, targetModel] = queryParam.split("-"); if (!leafIndex || !hookHash || !queryIndex || !direction || !cursor) return null; const parsedLeafIndex = Number.parseInt(leafIndex); const parsedHookHash = Number.parseInt(hookHash); const parsedQueryIndex = Number.parseInt(queryIndex); if (Number.isNaN(parsedLeafIndex) || Number.isNaN(parsedHookHash) || Number.isNaN(parsedQueryIndex)) return null; return { leafIndex: parsedLeafIndex, hookHash: parsedHookHash, queryIndex: parsedQueryIndex, direction: direction === "b" ? "before" : "after", cursor, targetModel }; }; /** * Safely attaches the `before` or `after` instruction to a query and creates another * query that determines how many records are available before or after the selected page * of records. * * @param query - The `Query` to which the instruction should be attached. * @param direction - The direction into which the pagination should go. * @param cursor - The value of the `before` or `after` instruction. * @param targetModel - If the query is used to address multiple models at once, the * model for which the current result was provided is passed here. * * @returns The updated main `Query` and the additional `Query` for counting. */ const paginateQuery = (query, direction, cursor, targetModel) => { const queryValue = Object.values(query)[0]; let querySchema = Object.keys(queryValue)[0]; if (queryValue[querySchema] === null) queryValue[querySchema] = {}; let queryInstructions = queryValue[querySchema]; if (targetModel) { const allQueryInstructions = queryInstructions; if (!allQueryInstructions["on"]) allQueryInstructions["on"] = {}; if (!allQueryInstructions["on"][targetModel]) allQueryInstructions["on"][targetModel] = {}; queryInstructions = allQueryInstructions["on"][targetModel]; querySchema = targetModel; } queryInstructions[direction] = cursor; return { query, countQuery: { count: { [querySchema]: { with: queryInstructions.with, orderedBy: queryInstructions.orderedBy, ...direction === "before" ? { after: cursor } : { before: cursor } } } } }; }; //#endregion //#region public/server/hooks.ts /** * Allows for populating the `<head>` element of the page with `<meta>` tags that help * the browser and crawlers obtain meta information about the currently active page, such * as the page title or Open Graph images. * * @param metadata - A `IncomingPageMetadata` object containing the meta information that * should be rendered in the `<head>` element. */ const useMetadata = (metadata) => { const serverContext = useContext(RootServerContext); if (!serverContext) throw new Error("Missing server context in `useMetadata`"); const { collected } = serverContext; const { title,...remainingMetadata } = structuredClone(metadata); if (title) { const titleSegments = collected.metadata.title ? Array.from(collected.metadata.title) : []; titleSegments.unshift(title); collected.metadata.title = new Set(titleSegments); } Object.assign(collected.metadata, remainingMetadata); }; /** * Formats a particular query result by applying pagination cursors to it. * * @param queries - The full list of queries being executed. * @param result - The result of the particular query being executed. * @param leafIndex - The index of the leaf (layout or page) in the layout tree. * @param hookHash - The hash of the `use` query hook that is being executed. * @param queryIndex - The index of the query that is being executed, within the `use` * query hook that is being executed. * @param targetModel - If the query is used to address multiple models at once, the * model for which the current result was provided is passed here. * * @returns The formatted query result. */ const formatResult = (queries, result, leafIndex, hookHash, queryIndex, targetModel) => { if (!Array.isArray(result)) return result; const resultArray = result; const paginationAmountQueryItem = queries.find((queryDetails) => { return queryDetails.paginationDetails?.countForQueryAtIndex === queryIndex; }); if (paginationAmountQueryItem) { const amount = paginationAmountQueryItem.result; const beforeAmount = amount === 0 ? amount : amount + 1; const { direction } = paginationAmountQueryItem.paginationDetails || {}; const propertyName = direction === "after" ? "beforeAmount" : "afterAmount"; resultArray[propertyName] = beforeAmount; } if (resultArray.moreBefore) { resultArray.moreBefore = `${leafIndex}-${hookHash}-${queryIndex}-b-${resultArray.moreBefore}`; if (targetModel) resultArray.moreBefore += `-${targetModel}`; } if (resultArray.moreAfter) { resultArray.moreAfter = `${leafIndex}-${hookHash}-${queryIndex}-a-${resultArray.moreAfter}`; if (targetModel) resultArray.moreAfter += `-${targetModel}`; } return result; }; const queryHandler = (queries) => { const serverContext = useContext(RootServerContext); if (!serverContext) throw new Error("Missing server context in `use`"); const hookHash = generateHashSync(JSON.stringify(queries)); const executedQueries = serverContext.collected.queries; const leafIndex = serverContext.currentLeafIndex; if (typeof leafIndex !== "number") throw new Error("Leaf index not available."); const page = parsePaginationQueryParam(new URL(serverContext.url).searchParams.get("page")); const formattedQueries = queries.flatMap(({ query, options }, queryIndex) => { const queryDetails = { ...options, type: "read" }; const multiModel = "all" in Object.values(query)[0]; if (page && page.leafIndex === leafIndex && page.hookHash === hookHash && page.queryIndex === queryIndex) { const { query: newQuery, countQuery } = paginateQuery(query, page.direction, page.cursor, page.targetModel); return [{ ...queryDetails, query: JSON.stringify(newQuery), multiModel }, { ...queryDetails, query: JSON.stringify(countQuery), paginationDetails: { countForQueryAtIndex: queryIndex, direction: page.direction } }]; } return { ...queryDetails, query: JSON.stringify(query), multiModel }; }).map((currentQuery) => { const executedQueryMatch = executedQueries.find(({ query, database }) => { return query === currentQuery.query && database === currentQuery.database; }); if (executedQueryMatch) { currentQuery.result = executedQueryMatch.result; currentQuery.error = executedQueryMatch.error; } return currentQuery; }); const emptyQueries = formattedQueries.filter(({ result, error }) => { return typeof result === "undefined" && typeof error === "undefined"; }); if (emptyQueries.length > 0) throw { __blade_queries: emptyQueries }; return formattedQueries.filter(({ paginationDetails }) => typeof paginationDetails === "undefined").map(({ result, error, multiModel }, queryIndex) => { if (typeof error !== "undefined") throw deserializeError(error); if (result && multiModel) return Object.fromEntries(Object.entries(result).map(([model, nestedResult]) => { return [model, formatResult(formattedQueries, nestedResult, leafIndex, hookHash, queryIndex, model)]; })); return formatResult(formattedQueries, result, leafIndex, hookHash, queryIndex); }); }; const callback = (defaultQuery, options) => { return queryHandler([{ query: defaultQuery[QUERY_SYMBOLS.QUERY], options }])[0]; }; /** Allows for retrieving records. */ const use = getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.get`, callback }); /** Allows for counting records. */ const useCountOf = getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.count`, callback }); /** Allows for retrieving models. */ const useListOf = getSyntaxProxy({ root: `${QUERY_SYMBOLS.QUERY}.list`, callback }); const useBatch = ((operations, options) => { const queriesWithIndexes = getBatchProxy(operations).map(({ structure, options: options$1 }) => { return { query: structure, options: options$1 }; }).map(({ query, options: options$1 }, index) => { if (!query) return { query, index }; return { query, options: options$1, index }; }); const queriesClean = queriesWithIndexes.filter(({ query }) => { return typeof query === "object" && query !== null; }); const queryResults = queryHandler(queriesClean.map(({ query, options: perQueryOptions }) => ({ query, options: perQueryOptions || options }))); return queriesWithIndexes.map(({ query, index }) => { const matchingQuery = queriesClean.findIndex((item) => item.index === index); if (matchingQuery > -1) return queryResults[matchingQuery]; return query; }); }); //#endregion export { use, useBatch, useCountOf, useListOf, useMetadata };