feat: implement pagination for fetching contacts and add maxObjectsInGet capability

This commit is contained in:
Linus Rath
2026-03-20 16:47:27 +01:00
parent dcc35335f5
commit 6cff98ddb8
+68 -37
View File
@@ -2066,6 +2066,11 @@ export class JMAPClient {
return coreCapability?.maxCallsInRequest || 50; return coreCapability?.maxCallsInRequest || 50;
} }
getMaxObjectsInGet(): number {
const coreCapability = this.capabilities["urn:ietf:params:jmap:core"] as { maxObjectsInGet?: number } | undefined;
return coreCapability?.maxObjectsInGet || 500;
}
getEventSourceUrl(): string | null { getEventSourceUrl(): string | null {
if (!this.session) return null; if (!this.session) return null;
@@ -2428,26 +2433,62 @@ export class JMAPClient {
} }
} }
async getContacts(addressBookId?: string): Promise<ContactCard[]> { private async fetchPaginatedContacts(
try { accountId: string,
const accountId = this.getContactsAccountId(); filter?: Record<string, unknown>,
const queryArgs: Record<string, unknown> = { accountId, limit: 1000 }; ): Promise<ContactCard[]> {
if (addressBookId) { const batchSize = this.getMaxObjectsInGet();
queryArgs.filter = { inAddressBook: addressBookId }; const allIds: string[] = [];
let position = 0;
// Paginate ContactCard/query to collect all IDs
for (;;) {
const queryArgs: Record<string, unknown> = { accountId, position, limit: batchSize };
if (filter) {
queryArgs.filter = filter;
} }
const response = await this.request([ const response = await this.request([
["ContactCard/query", queryArgs, "0"], ["ContactCard/query", queryArgs, "q"],
["ContactCard/get", {
accountId,
"#ids": { resultOf: "0", name: "ContactCard/query", path: "/ids" },
}, "1"],
], this.contactUsing()); ], this.contactUsing());
if (response.methodResponses?.[1]?.[0] === "ContactCard/get") { const queryResult = response.methodResponses?.[0];
return (response.methodResponses[1][1].list || []) as ContactCard[]; if (queryResult?.[0] !== "ContactCard/query") break;
const ids: string[] = queryResult[1].ids || [];
allIds.push(...ids);
const total: number = queryResult[1].total ?? -1;
if (ids.length < batchSize || (total > 0 && allIds.length >= total)) {
break;
} }
return []; position += ids.length;
}
if (allIds.length === 0) return [];
// Batch ContactCard/get to respect maxObjectsInGet
const allContacts: ContactCard[] = [];
for (let i = 0; i < allIds.length; i += batchSize) {
const chunk = allIds.slice(i, i + batchSize);
const response = await this.request([
["ContactCard/get", { accountId, ids: chunk }, "g"],
], this.contactUsing());
if (response.methodResponses?.[0]?.[0] === "ContactCard/get") {
const list = (response.methodResponses[0][1].list || []) as ContactCard[];
allContacts.push(...list);
}
}
return allContacts;
}
async getContacts(addressBookId?: string): Promise<ContactCard[]> {
try {
const accountId = this.getContactsAccountId();
const filter = addressBookId ? { inAddressBook: addressBookId } : undefined;
return await this.fetchPaginatedContacts(accountId, filter);
} catch (error) { } catch (error) {
console.error('Failed to get contacts:', error); console.error('Failed to get contacts:', error);
return []; return [];
@@ -2465,29 +2506,19 @@ export class JMAPClient {
const account = this.accounts[accountId]; const account = this.accounts[accountId];
try { try {
const response = await this.request([ const rawContacts = await this.fetchPaginatedContacts(accountId);
["ContactCard/query", { accountId, limit: 1000 }, "0"], const contacts = rawContacts.map((contact) => ({
["ContactCard/get", { ...contact,
accountId, id: isPrimary ? contact.id : `${accountId}:${contact.id}`,
"#ids": { resultOf: "0", name: "ContactCard/query", path: "/ids" }, originalId: contact.id,
}, "1"], addressBookIds: isPrimary ? contact.addressBookIds : (contact.addressBookIds ? Object.fromEntries(
], this.contactUsing()); Object.entries(contact.addressBookIds).map(([bookId, v]) => [`${accountId}:${bookId}`, v])
) : contact.addressBookIds),
if (response.methodResponses?.[1]?.[0] === "ContactCard/get") { accountId,
const rawContacts = (response.methodResponses[1][1].list || []) as ContactCard[]; accountName: account?.name || (isPrimary ? this.username : accountId),
const contacts = rawContacts.map((contact) => ({ isShared: !isPrimary,
...contact, }));
id: isPrimary ? contact.id : `${accountId}:${contact.id}`, allContacts.push(...contacts);
originalId: contact.id,
addressBookIds: isPrimary ? contact.addressBookIds : (contact.addressBookIds ? Object.fromEntries(
Object.entries(contact.addressBookIds).map(([bookId, v]) => [`${accountId}:${bookId}`, v])
) : contact.addressBookIds),
accountId,
accountName: account?.name || (isPrimary ? this.username : accountId),
isShared: !isPrimary,
}));
allContacts.push(...contacts);
}
} catch (error) { } catch (error) {
console.error(`Failed to fetch contacts for account ${accountId}:`, error); console.error(`Failed to fetch contacts for account ${accountId}:`, error);
} }