search infinite scroll!
This commit is contained in:
parent
4045844e4b
commit
5e3071db4f
4 changed files with 444 additions and 126 deletions
|
@ -7,7 +7,12 @@ import { Delay, Heading1, useDebounce } from "./util";
|
||||||
import ItemList, { ItemListSkeleton } from "./ItemList";
|
import ItemList, { ItemListSkeleton } from "./ItemList";
|
||||||
import { itemAppearanceFragment } from "./OutfitPreview";
|
import { itemAppearanceFragment } from "./OutfitPreview";
|
||||||
|
|
||||||
function SearchPanel({ query, outfitState, dispatchToOutfit }) {
|
function SearchPanel({
|
||||||
|
query,
|
||||||
|
outfitState,
|
||||||
|
dispatchToOutfit,
|
||||||
|
getScrollParent,
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box color="green.800">
|
<Box color="green.800">
|
||||||
<Heading1 mb="4">Searching for "{query}"</Heading1>
|
<Heading1 mb="4">Searching for "{query}"</Heading1>
|
||||||
|
@ -15,6 +20,7 @@ function SearchPanel({ query, outfitState, dispatchToOutfit }) {
|
||||||
query={query}
|
query={query}
|
||||||
outfitState={outfitState}
|
outfitState={outfitState}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
|
getScrollParent={getScrollParent}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -25,14 +31,18 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
||||||
|
|
||||||
const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true });
|
const debouncedQuery = useDebounce(query, 300, { waitForFirstPause: true });
|
||||||
|
|
||||||
const { loading, error, data, variables } = useQuery(
|
const { loading, error, data, fetchMore, variables } = useQuery(
|
||||||
gql`
|
gql`
|
||||||
query($query: String!, $speciesId: ID!, $colorId: ID!) {
|
query($query: String!, $speciesId: ID!, $colorId: ID!, $offset: Int!) {
|
||||||
itemSearchToFit(
|
itemSearchToFit(
|
||||||
query: $query
|
query: $query
|
||||||
speciesId: $speciesId
|
speciesId: $speciesId
|
||||||
colorId: $colorId
|
colorId: $colorId
|
||||||
|
offset: $offset
|
||||||
|
limit: 50
|
||||||
) {
|
) {
|
||||||
|
query
|
||||||
|
items {
|
||||||
# TODO: De-dupe this from useOutfitState?
|
# TODO: De-dupe this from useOutfitState?
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
@ -53,15 +63,44 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
${itemAppearanceFragment}
|
${itemAppearanceFragment}
|
||||||
`,
|
`,
|
||||||
{
|
{
|
||||||
variables: { query: debouncedQuery, speciesId, colorId },
|
variables: { query: debouncedQuery, speciesId, colorId, offset: 0 },
|
||||||
skip: debouncedQuery === null,
|
skip: debouncedQuery === null,
|
||||||
|
notifyOnNetworkStatusChange: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading || variables.query !== query) {
|
const result = data && data.itemSearchToFit;
|
||||||
|
const resultQuery = result && result.query;
|
||||||
|
const items = (result && result.items) || [];
|
||||||
|
|
||||||
|
const onScrolledToBottom = React.useCallback(() => {
|
||||||
|
if (!loading) {
|
||||||
|
fetchMore({
|
||||||
|
variables: {
|
||||||
|
offset: items.length,
|
||||||
|
},
|
||||||
|
updateQuery: (prev, { fetchMoreResult }) => {
|
||||||
|
if (!fetchMoreResult) return prev;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
itemSearchToFit: {
|
||||||
|
...prev.itemSearchToFit,
|
||||||
|
items: [
|
||||||
|
...prev.itemSearchToFit.items,
|
||||||
|
...fetchMoreResult.itemSearchToFit.items,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [loading, fetchMore, items.length]);
|
||||||
|
|
||||||
|
if (resultQuery !== query || (loading && items.length === 0)) {
|
||||||
return (
|
return (
|
||||||
<Delay ms={500}>
|
<Delay ms={500}>
|
||||||
<ItemListSkeleton count={8} />
|
<ItemListSkeleton count={8} />
|
||||||
|
@ -81,8 +120,6 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = data.itemSearchToFit;
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Text color="green.500">
|
<Text color="green.500">
|
||||||
|
@ -96,12 +133,56 @@ function SearchResults({ query, outfitState, dispatchToOutfit }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ScrollTracker threshold={300} onScrolledToBottom={onScrolledToBottom}>
|
||||||
<ItemList
|
<ItemList
|
||||||
items={items}
|
items={items}
|
||||||
outfitState={outfitState}
|
outfitState={outfitState}
|
||||||
dispatchToOutfit={dispatchToOutfit}
|
dispatchToOutfit={dispatchToOutfit}
|
||||||
/>
|
/>
|
||||||
|
{items && loading && <ItemListSkeleton count={8} />}
|
||||||
|
</ScrollTracker>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ScrollTracker({ children, threshold, onScrolledToBottom }) {
|
||||||
|
const containerRef = React.useRef();
|
||||||
|
const scrollParent = React.useRef();
|
||||||
|
|
||||||
|
const onScroll = React.useCallback(
|
||||||
|
(e) => {
|
||||||
|
const topEdgeScrollPosition = e.target.scrollTop;
|
||||||
|
const bottomEdgeScrollPosition =
|
||||||
|
topEdgeScrollPosition + e.target.clientHeight;
|
||||||
|
const remainingScrollDistance =
|
||||||
|
e.target.scrollHeight - bottomEdgeScrollPosition;
|
||||||
|
if (remainingScrollDistance < threshold) {
|
||||||
|
onScrolledToBottom();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onScrolledToBottom, threshold]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useLayoutEffect(() => {
|
||||||
|
if (!containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let el = containerRef.current; el.parentNode; el = el.parentNode) {
|
||||||
|
if (el.scrollHeight > el.clientHeight) {
|
||||||
|
scrollParent.current = el;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollParent.current.addEventListener("scroll", onScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (scrollParent.current) {
|
||||||
|
scrollParent.current.removeEventListener("scroll", onScroll);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
return <Box ref={containerRef}>{children}</Box>;
|
||||||
|
}
|
||||||
|
|
||||||
export default SearchPanel;
|
export default SearchPanel;
|
||||||
|
|
|
@ -34,10 +34,21 @@ const typeDefs = gql`
|
||||||
label: String!
|
label: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemSearchResult {
|
||||||
|
query: String!
|
||||||
|
items: [Item!]!
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
items(ids: [ID!]!): [Item!]!
|
items(ids: [ID!]!): [Item!]!
|
||||||
itemSearch(query: String!): [Item!]!
|
itemSearch(query: String!): ItemSearchResult!
|
||||||
itemSearchToFit(query: String!, speciesId: ID!, colorId: ID!): [Item!]!
|
itemSearchToFit(
|
||||||
|
query: String!
|
||||||
|
speciesId: ID!
|
||||||
|
colorId: ID!
|
||||||
|
offset: Int
|
||||||
|
limit: Int
|
||||||
|
): ItemSearchResult!
|
||||||
petAppearance(speciesId: ID!, colorId: ID!): Appearance
|
petAppearance(speciesId: ID!, colorId: ID!): Appearance
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
@ -114,17 +125,22 @@ const resolvers = {
|
||||||
},
|
},
|
||||||
itemSearch: async (_, { query }, { itemSearchLoader }) => {
|
itemSearch: async (_, { query }, { itemSearchLoader }) => {
|
||||||
const items = await itemSearchLoader.load(query);
|
const items = await itemSearchLoader.load(query);
|
||||||
return items;
|
return { query, items };
|
||||||
},
|
},
|
||||||
itemSearchToFit: async (
|
itemSearchToFit: async (
|
||||||
_,
|
_,
|
||||||
{ query, speciesId, colorId },
|
{ query, speciesId, colorId, offset, limit },
|
||||||
{ petTypeLoader, itemSearchToFitLoader }
|
{ petTypeLoader, itemSearchToFitLoader }
|
||||||
) => {
|
) => {
|
||||||
const petType = await petTypeLoader.load({ speciesId, colorId });
|
const petType = await petTypeLoader.load({ speciesId, colorId });
|
||||||
const { bodyId } = petType;
|
const { bodyId } = petType;
|
||||||
const items = await itemSearchToFitLoader.load({ query, bodyId });
|
const items = await itemSearchToFitLoader.load({
|
||||||
return items;
|
query,
|
||||||
|
bodyId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
return { query, items };
|
||||||
},
|
},
|
||||||
petAppearance: async (
|
petAppearance: async (
|
||||||
_,
|
_,
|
||||||
|
|
|
@ -376,17 +376,21 @@ describe("Search", () => {
|
||||||
query: gql`
|
query: gql`
|
||||||
query {
|
query {
|
||||||
itemSearch(query: "Neopian Times") {
|
itemSearch(query: "Neopian Times") {
|
||||||
|
query
|
||||||
|
items {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res).toHaveNoErrors();
|
expect(res).toHaveNoErrors();
|
||||||
expect(res.data).toMatchInlineSnapshot(`
|
expect(res.data).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"itemSearch": Array [
|
"itemSearch": Object {
|
||||||
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "40431",
|
"id": "40431",
|
||||||
"name": "Neopian Times Background",
|
"name": "Neopian Times Background",
|
||||||
|
@ -436,6 +440,8 @@ describe("Search", () => {
|
||||||
"name": "Neopian Times Zafara Trousers",
|
"name": "Neopian Times Zafara Trousers",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"query": "Neopian Times",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
@ -463,17 +469,21 @@ describe("Search", () => {
|
||||||
speciesId: "54"
|
speciesId: "54"
|
||||||
colorId: "75"
|
colorId: "75"
|
||||||
) {
|
) {
|
||||||
|
query
|
||||||
|
items {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res).toHaveNoErrors();
|
expect(res).toHaveNoErrors();
|
||||||
expect(res.data).toMatchInlineSnapshot(`
|
expect(res.data).toMatchInlineSnapshot(`
|
||||||
Object {
|
Object {
|
||||||
"itemSearchToFit": Array [
|
"itemSearchToFit": Object {
|
||||||
|
"items": Array [
|
||||||
Object {
|
Object {
|
||||||
"id": "40431",
|
"id": "40431",
|
||||||
"name": "Neopian Times Background",
|
"name": "Neopian Times Background",
|
||||||
|
@ -503,6 +513,8 @@ describe("Search", () => {
|
||||||
"name": "Neopian Times Zafara Trousers",
|
"name": "Neopian Times Zafara Trousers",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
"query": "Neopian Times",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
@ -523,10 +535,214 @@ describe("Search", () => {
|
||||||
WHERE t.name LIKE ? AND t.locale=\\"en\\" AND
|
WHERE t.name LIKE ? AND t.locale=\\"en\\" AND
|
||||||
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
||||||
ORDER BY t.name
|
ORDER BY t.name
|
||||||
LIMIT 30",
|
LIMIT ? OFFSET ?",
|
||||||
Array [
|
Array [
|
||||||
"%Neopian Times%",
|
"%Neopian Times%",
|
||||||
"180",
|
"180",
|
||||||
|
30,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads the first 10 hats that fit the Starry Zafara", async () => {
|
||||||
|
const res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
itemSearchToFit(
|
||||||
|
query: "hat"
|
||||||
|
speciesId: "54"
|
||||||
|
colorId: "75"
|
||||||
|
offset: 0
|
||||||
|
limit: 10
|
||||||
|
) {
|
||||||
|
query
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"itemSearchToFit": Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "74967",
|
||||||
|
"name": "17th Birthday Party Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "49026",
|
||||||
|
"name": "Abominable Snowman Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "67242",
|
||||||
|
"name": "Accessories Shop Wig and Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "67242",
|
||||||
|
"name": "Accessories Shop Wig and Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "64177",
|
||||||
|
"name": "Acorn Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "69995",
|
||||||
|
"name": "Adventure in Pastel Hat and Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "69995",
|
||||||
|
"name": "Adventure in Pastel Hat and Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "62375",
|
||||||
|
"name": "Altador Cup Trophy Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "56654",
|
||||||
|
"name": "Altador Team Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "62322",
|
||||||
|
"name": "Altador Team Jester Hat",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"query": "hat",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
"SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)",
|
||||||
|
Array [
|
||||||
|
"54",
|
||||||
|
"75",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
"SELECT items.*, t.name FROM items
|
||||||
|
INNER JOIN item_translations t ON t.item_id = items.id
|
||||||
|
INNER JOIN parents_swf_assets rel
|
||||||
|
ON rel.parent_type = \\"Item\\" AND rel.parent_id = items.id
|
||||||
|
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
|
||||||
|
WHERE t.name LIKE ? AND t.locale=\\"en\\" AND
|
||||||
|
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
||||||
|
ORDER BY t.name
|
||||||
|
LIMIT ? OFFSET ?",
|
||||||
|
Array [
|
||||||
|
"%hat%",
|
||||||
|
"180",
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads the next 10 hats that fit the Starry Zafara", async () => {
|
||||||
|
const res = await query({
|
||||||
|
query: gql`
|
||||||
|
query {
|
||||||
|
itemSearchToFit(
|
||||||
|
query: "hat"
|
||||||
|
speciesId: "54"
|
||||||
|
colorId: "75"
|
||||||
|
offset: 10
|
||||||
|
limit: 10
|
||||||
|
) {
|
||||||
|
query
|
||||||
|
items {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(res).toHaveNoErrors();
|
||||||
|
expect(res.data).toMatchInlineSnapshot(`
|
||||||
|
Object {
|
||||||
|
"itemSearchToFit": Object {
|
||||||
|
"items": Array [
|
||||||
|
Object {
|
||||||
|
"id": "58733",
|
||||||
|
"name": "Apple Bobbing Bart Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "80401",
|
||||||
|
"name": "Aurricks Finest Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "80401",
|
||||||
|
"name": "Aurricks Finest Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "50168",
|
||||||
|
"name": "Babaa Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "78311",
|
||||||
|
"name": "Backwards Hat and Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "78311",
|
||||||
|
"name": "Backwards Hat and Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "66653",
|
||||||
|
"name": "Bagel Hat Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "66653",
|
||||||
|
"name": "Bagel Hat Wig",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "51366",
|
||||||
|
"name": "Balloon Sculpture Hat",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"id": "51366",
|
||||||
|
"name": "Balloon Sculpture Hat",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"query": "hat",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
expect(queryFn.mock.calls).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Array [
|
||||||
|
"SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)",
|
||||||
|
Array [
|
||||||
|
"54",
|
||||||
|
"75",
|
||||||
|
],
|
||||||
|
],
|
||||||
|
Array [
|
||||||
|
"SELECT items.*, t.name FROM items
|
||||||
|
INNER JOIN item_translations t ON t.item_id = items.id
|
||||||
|
INNER JOIN parents_swf_assets rel
|
||||||
|
ON rel.parent_type = \\"Item\\" AND rel.parent_id = items.id
|
||||||
|
INNER JOIN swf_assets ON rel.swf_asset_id = swf_assets.id
|
||||||
|
WHERE t.name LIKE ? AND t.locale=\\"en\\" AND
|
||||||
|
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
||||||
|
ORDER BY t.name
|
||||||
|
LIMIT ? OFFSET ?",
|
||||||
|
Array [
|
||||||
|
"%hat%",
|
||||||
|
"180",
|
||||||
|
10,
|
||||||
|
10,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
|
@ -64,7 +64,11 @@ const buildItemSearchToFitLoader = (db) =>
|
||||||
new DataLoader(async (queryAndBodyIdPairs) => {
|
new DataLoader(async (queryAndBodyIdPairs) => {
|
||||||
// This isn't actually optimized as a batch query, we're just using a
|
// This isn't actually optimized as a batch query, we're just using a
|
||||||
// DataLoader API consistency with our other loaders!
|
// DataLoader API consistency with our other loaders!
|
||||||
const queryPromises = queryAndBodyIdPairs.map(async ({ query, bodyId }) => {
|
const queryPromises = queryAndBodyIdPairs.map(
|
||||||
|
async ({ query, bodyId, offset, limit }) => {
|
||||||
|
const actualOffset = offset || 0;
|
||||||
|
const actualLimit = Math.min(limit || 30, 30);
|
||||||
|
|
||||||
const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%";
|
const queryForMysql = "%" + query.replace(/_%/g, "\\$0") + "%";
|
||||||
const [rows, _] = await db.execute(
|
const [rows, _] = await db.execute(
|
||||||
`SELECT items.*, t.name FROM items
|
`SELECT items.*, t.name FROM items
|
||||||
|
@ -75,14 +79,15 @@ const buildItemSearchToFitLoader = (db) =>
|
||||||
WHERE t.name LIKE ? AND t.locale="en" AND
|
WHERE t.name LIKE ? AND t.locale="en" AND
|
||||||
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
(swf_assets.body_id = ? OR swf_assets.body_id = 0)
|
||||||
ORDER BY t.name
|
ORDER BY t.name
|
||||||
LIMIT 30`,
|
LIMIT ? OFFSET ?`,
|
||||||
[queryForMysql, bodyId]
|
[queryForMysql, bodyId, actualLimit, actualOffset]
|
||||||
);
|
);
|
||||||
|
|
||||||
const entities = rows.map(normalizeRow);
|
const entities = rows.map(normalizeRow);
|
||||||
|
|
||||||
return entities;
|
return entities;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const responses = await Promise.all(queryPromises);
|
const responses = await Promise.all(queryPromises);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue