fix(blog): return the exact window for non-aligned offsets
Payload paginates by page, so floor(offset/limit)+1 alone returned the wrong window whenever offset % limit != 0 (B9). Fetch the straddled pages and slice the intra-page remainder. The test stub now honours page like real Payload (it silently ignored it before), and the contract pins aligned + non-aligned windows on mock and stub alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,46 @@ export const articlesRepositoryContract =
|
||||
expect(result[0]?.authorId).toBe("author-a");
|
||||
});
|
||||
|
||||
it("getArticles returns the exact window for an aligned offset", async () => {
|
||||
const created = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
created.push(await repo.createArticle(articleFactory.build()));
|
||||
}
|
||||
const window = await repo.getArticles({ offset: 2, limit: 2 });
|
||||
expect(window.map((a) => a.id)).toEqual([
|
||||
created[2]?.id,
|
||||
created[3]?.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it("getArticles returns the exact window for a non-aligned offset", async () => {
|
||||
const created = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
created.push(await repo.createArticle(articleFactory.build()));
|
||||
}
|
||||
// offset 3 with limit 2 straddles two limit-sized pages
|
||||
const window = await repo.getArticles({ offset: 3, limit: 2 });
|
||||
expect(window.map((a) => a.id)).toEqual([
|
||||
created[3]?.id,
|
||||
created[4]?.id,
|
||||
]);
|
||||
});
|
||||
|
||||
it("getArticles non-aligned offset near the end returns only the remaining items", async () => {
|
||||
const created = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
created.push(await repo.createArticle(articleFactory.build()));
|
||||
}
|
||||
const window = await repo.getArticles({ offset: 4, limit: 3 });
|
||||
expect(window.map((a) => a.id)).toEqual([created[4]?.id]);
|
||||
});
|
||||
|
||||
it("getArticles offset past the end returns an empty array", async () => {
|
||||
await repo.createArticle(articleFactory.build());
|
||||
const window = await repo.getArticles({ offset: 7, limit: 3 });
|
||||
expect(window).toEqual([]);
|
||||
});
|
||||
|
||||
// --- updateArticle ---
|
||||
|
||||
it("updateArticle changes fields and returns updated article", async () => {
|
||||
|
||||
@@ -33,6 +33,7 @@ function buildPayloadStub() {
|
||||
async ({
|
||||
where,
|
||||
limit,
|
||||
page,
|
||||
}: {
|
||||
collection: string;
|
||||
where?: {
|
||||
@@ -54,17 +55,27 @@ function buildPayloadStub() {
|
||||
if (where?.author) {
|
||||
docs = docs.filter((d) => d.author === where.author?.equals);
|
||||
}
|
||||
if (limit !== undefined) {
|
||||
docs = docs.slice(0, limit);
|
||||
}
|
||||
// Mirror real Payload pagination: page-based windows of size `limit`.
|
||||
const lim = limit ?? 50;
|
||||
const pg = page ?? 1;
|
||||
const start = (pg - 1) * lim;
|
||||
docs = docs.slice(start, start + lim);
|
||||
return { docs };
|
||||
},
|
||||
),
|
||||
findByID: vi.fn(
|
||||
async ({ id }: { collection: string; id: string; overrideAccess?: boolean }) => {
|
||||
async ({
|
||||
id,
|
||||
}: {
|
||||
collection: string;
|
||||
id: string;
|
||||
overrideAccess?: boolean;
|
||||
}) => {
|
||||
const doc = store.get(String(id));
|
||||
if (!doc) {
|
||||
const err = Object.assign(new Error(`Not found: ${id}`), { status: 404 });
|
||||
const err = Object.assign(new Error(`Not found: ${id}`), {
|
||||
status: 404,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
return doc;
|
||||
@@ -82,7 +93,9 @@ function buildPayloadStub() {
|
||||
}) => {
|
||||
const existing = store.get(String(id));
|
||||
if (!existing) {
|
||||
const err = Object.assign(new Error(`Not found: ${id}`), { status: 404 });
|
||||
const err = Object.assign(new Error(`Not found: ${id}`), {
|
||||
status: 404,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
const updated = { ...existing, ...data };
|
||||
|
||||
@@ -88,7 +88,10 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
this.logger.captureException(err, {
|
||||
tags: { feature: FEATURE, repo: REPO, method: "getArticle" },
|
||||
});
|
||||
span.setStatus("error", err instanceof Error ? err.message : String(err));
|
||||
span.setStatus(
|
||||
"error",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -97,7 +100,11 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
|
||||
async getArticleBySlug(slug: string): Promise<Article | undefined> {
|
||||
return this.tracer.startSpan(
|
||||
{ name: "articles.getArticleBySlug", op: "repository", attributes: { slug } },
|
||||
{
|
||||
name: "articles.getArticleBySlug",
|
||||
op: "repository",
|
||||
attributes: { slug },
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
const payload = await getPayload({ config: this.config });
|
||||
@@ -114,7 +121,10 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
this.logger.captureException(err, {
|
||||
tags: { feature: FEATURE, repo: REPO, method: "getArticleBySlug" },
|
||||
});
|
||||
span.setStatus("error", err instanceof Error ? err.message : String(err));
|
||||
span.setStatus(
|
||||
"error",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -145,22 +155,42 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
if (options?.status) where.status = { equals: options.status };
|
||||
if (options?.authorId) where.author = { equals: options.authorId };
|
||||
|
||||
const result = await payload.find({
|
||||
collection: "articles",
|
||||
where: where as never,
|
||||
limit: options?.limit ?? 50,
|
||||
page: options?.offset
|
||||
? Math.floor(options.offset / (options.limit ?? 50)) + 1
|
||||
: 1,
|
||||
overrideAccess: true,
|
||||
});
|
||||
span.setAttribute("count", result.docs.length);
|
||||
return result.docs.map((d) => mapDoc(d as PayloadArticleDoc));
|
||||
const limit = options?.limit ?? 50;
|
||||
const offset = options?.offset ?? 0;
|
||||
// Payload paginates by page, not offset. A non-aligned offset
|
||||
// (offset % limit !== 0) straddles two pages, so fetch both and
|
||||
// slice out the exact [offset, offset + limit) window.
|
||||
const page = limit > 0 ? Math.floor(offset / limit) + 1 : 1;
|
||||
const remainder = limit > 0 ? offset % limit : 0;
|
||||
|
||||
const findPage = (p: number) =>
|
||||
payload.find({
|
||||
collection: "articles",
|
||||
where: where as never,
|
||||
limit,
|
||||
page: p,
|
||||
overrideAccess: true,
|
||||
});
|
||||
|
||||
const first = await findPage(page);
|
||||
let docs = first.docs;
|
||||
if (remainder > 0) {
|
||||
if (docs.length === limit) {
|
||||
const second = await findPage(page + 1);
|
||||
docs = [...docs, ...second.docs];
|
||||
}
|
||||
docs = docs.slice(remainder, remainder + limit);
|
||||
}
|
||||
span.setAttribute("count", docs.length);
|
||||
return docs.map((d) => mapDoc(d as PayloadArticleDoc));
|
||||
} catch (err) {
|
||||
this.logger.captureException(err, {
|
||||
tags: { feature: FEATURE, repo: REPO, method: "getArticles" },
|
||||
});
|
||||
span.setStatus("error", err instanceof Error ? err.message : String(err));
|
||||
span.setStatus(
|
||||
"error",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -169,7 +199,11 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
|
||||
async createArticle(input: Article): Promise<Article> {
|
||||
return this.tracer.startSpan(
|
||||
{ name: "articles.createArticle", op: "repository", attributes: { slug: input.slug } },
|
||||
{
|
||||
name: "articles.createArticle",
|
||||
op: "repository",
|
||||
attributes: { slug: input.slug },
|
||||
},
|
||||
async (span) => {
|
||||
try {
|
||||
const payload = await getPayload({ config: this.config });
|
||||
@@ -190,7 +224,10 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
this.logger.captureException(err, {
|
||||
tags: { feature: FEATURE, repo: REPO, method: "createArticle" },
|
||||
});
|
||||
span.setStatus("error", err instanceof Error ? err.message : String(err));
|
||||
span.setStatus(
|
||||
"error",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -233,7 +270,10 @@ export class ArticlesRepository implements IArticlesRepository {
|
||||
this.logger.captureException(err, {
|
||||
tags: { feature: FEATURE, repo: REPO, method: "updateArticle" },
|
||||
});
|
||||
span.setStatus("error", err instanceof Error ? err.message : String(err));
|
||||
span.setStatus(
|
||||
"error",
|
||||
err instanceof Error ? err.message : String(err),
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user