blade
Version:
React at the edge.
242 lines (239 loc) • 9.58 kB
JavaScript
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 };