diff --git a/lib/api.js b/lib/api.js index 828ef59..9065355 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,6 +1,105 @@ const fetch = require('node-fetch'); const config = require('./config'); +function createCondition({ property, operator, value }) { + return { + type: 'condition', + id: `cond_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`, + property, + operator, + value + }; +} + +function normalizeDateOnly(value) { + if (typeof value !== 'string') return value; + const tIndex = value.indexOf('T'); + if (tIndex === -1) return value; + return value.slice(0, tIndex); +} + +function parseBooleanLike(value) { + if (typeof value === 'boolean') return value; + if (typeof value === 'number') return value !== 0; + if (typeof value === 'string') return value.toLowerCase() === 'true' || value === '1'; + return false; +} + +function legacyListFiltersToFilterQuery(filters = {}) { + const query = { + type: 'group', + id: `root_${Date.now()}`, + conjunction: 'and', + children: [] + }; + + // Legacy GET /api/tasks query params are no longer supported for filtering. + // Convert common CLI filters to POST /api/tasks/query FilterQuery objects. + if (filters.completed !== undefined) { + const isCompleted = parseBooleanLike(filters.completed); + query.children.push( + createCondition({ + property: 'status.isCompleted', + operator: isCompleted ? 'is-checked' : 'is-not-checked', + value: null + }) + ); + } + + if (filters.archived !== undefined) { + const isArchived = parseBooleanLike(filters.archived); + query.children.push( + createCondition({ + property: 'archived', + operator: isArchived ? 'is-checked' : 'is-not-checked', + value: null + }) + ); + } + + if (filters.project !== undefined && filters.project !== null && String(filters.project).trim()) { + query.children.push( + createCondition({ + property: 'projects', + operator: 'contains', + value: String(filters.project) + }) + ); + } + + if (filters.scheduled_after !== undefined && filters.scheduled_after !== null && String(filters.scheduled_after).trim()) { + query.children.push( + createCondition({ + property: 'scheduled', + operator: 'is-on-or-after', + value: normalizeDateOnly(String(filters.scheduled_after)) + }) + ); + } + + if (filters.scheduled_before !== undefined && filters.scheduled_before !== null && String(filters.scheduled_before).trim()) { + query.children.push( + createCondition({ + property: 'scheduled', + operator: 'is-on-or-before', + value: normalizeDateOnly(String(filters.scheduled_before)) + }) + ); + } + + if (filters.due_before !== undefined && filters.due_before !== null && String(filters.due_before).trim()) { + query.children.push( + createCondition({ + property: 'due', + operator: 'is-before', + value: String(filters.due_before) + }) + ); + } + + return query; +} + class TaskNotesAPI { constructor() { this.config = config.get(); @@ -81,18 +180,18 @@ class TaskNotesAPI { } async listTasks(filters = {}) { - const params = new URLSearchParams(); - - Object.entries(filters).forEach(([key, value]) => { - if (value !== undefined && value !== null) { - params.append(key, value); + const { limit, ...legacyFilters } = filters || {}; + const filterQuery = legacyListFiltersToFilterQuery(legacyFilters); + const result = await this.queryTasks(filterQuery); + + if (limit !== undefined && limit !== null) { + const parsedLimit = parseInt(limit, 10); + if (!Number.isNaN(parsedLimit) && parsedLimit >= 0 && Array.isArray(result.tasks)) { + return { ...result, tasks: result.tasks.slice(0, parsedLimit) }; } - }); + } - const queryString = params.toString(); - const endpoint = `/api/tasks${queryString ? `?${queryString}` : ''}`; - - return this.request(endpoint); + return result; } async queryTasks(filterQuery) { @@ -319,4 +418,4 @@ class TaskNotesAPI { } } -module.exports = TaskNotesAPI; \ No newline at end of file +module.exports = TaskNotesAPI;