feat: add api-storage improvements (#1278)
This commit is contained in:
@@ -246,7 +246,7 @@ export class ListTasksCommand extends Command {
|
|||||||
task.subtasks.forEach((subtask) => {
|
task.subtasks.forEach((subtask) => {
|
||||||
const subIcon = STATUS_ICONS[subtask.status];
|
const subIcon = STATUS_ICONS[subtask.status];
|
||||||
console.log(
|
console.log(
|
||||||
` ${chalk.gray(`${task.id}.${subtask.id}`)} ${subIcon} ${chalk.gray(subtask.title)}`
|
` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ export class ListTasksCommand extends Command {
|
|||||||
nextTask
|
nextTask
|
||||||
);
|
);
|
||||||
|
|
||||||
// Task table - no title, just show the table directly
|
// Task table
|
||||||
console.log(
|
console.log(
|
||||||
ui.createTaskTable(tasks, {
|
ui.createTaskTable(tasks, {
|
||||||
showSubtasks: withSubtasks,
|
showSubtasks: withSubtasks,
|
||||||
|
|||||||
@@ -258,9 +258,6 @@ export class SetStatusCommand extends Command {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show storage info
|
|
||||||
console.log(chalk.gray(`\nUsing ${result.storageType} storage`));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -192,8 +192,7 @@ export function displaySubtasks(
|
|||||||
status: any;
|
status: any;
|
||||||
description?: string;
|
description?: string;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
}>,
|
}>
|
||||||
parentId: string | number
|
|
||||||
): void {
|
): void {
|
||||||
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
const terminalWidth = process.stdout.columns * 0.95 || 100;
|
||||||
// Display subtasks header
|
// Display subtasks header
|
||||||
@@ -228,7 +227,7 @@ export function displaySubtasks(
|
|||||||
});
|
});
|
||||||
|
|
||||||
subtasks.forEach((subtask) => {
|
subtasks.forEach((subtask) => {
|
||||||
const subtaskId = `${parentId}.${subtask.id}`;
|
const subtaskId = String(subtask.id);
|
||||||
|
|
||||||
// Format dependencies
|
// Format dependencies
|
||||||
const deps =
|
const deps =
|
||||||
@@ -329,7 +328,7 @@ export function displayTaskDetails(
|
|||||||
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
|
||||||
} else if (filteredSubtasks.length > 0) {
|
} else if (filteredSubtasks.length > 0) {
|
||||||
console.log(); // Empty line for spacing
|
console.log(); // Empty line for spacing
|
||||||
displaySubtasks(filteredSubtasks, task.id);
|
displaySubtasks(filteredSubtasks);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -286,12 +286,12 @@ export function createTaskTable(
|
|||||||
// Adjust column widths to better match the original layout
|
// Adjust column widths to better match the original layout
|
||||||
const baseColWidths = showComplexity
|
const baseColWidths = showComplexity
|
||||||
? [
|
? [
|
||||||
Math.floor(terminalWidth * 0.06),
|
Math.floor(terminalWidth * 0.1),
|
||||||
Math.floor(terminalWidth * 0.4),
|
Math.floor(terminalWidth * 0.4),
|
||||||
Math.floor(terminalWidth * 0.15),
|
Math.floor(terminalWidth * 0.15),
|
||||||
Math.floor(terminalWidth * 0.12),
|
Math.floor(terminalWidth * 0.1),
|
||||||
Math.floor(terminalWidth * 0.2),
|
Math.floor(terminalWidth * 0.2),
|
||||||
Math.floor(terminalWidth * 0.12)
|
Math.floor(terminalWidth * 0.1)
|
||||||
] // ID, Title, Status, Priority, Dependencies, Complexity
|
] // ID, Title, Status, Priority, Dependencies, Complexity
|
||||||
: [
|
: [
|
||||||
Math.floor(terminalWidth * 0.08),
|
Math.floor(terminalWidth * 0.08),
|
||||||
@@ -377,7 +377,11 @@ export function createTaskTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showComplexity) {
|
if (showComplexity) {
|
||||||
subRow.push(chalk.gray('--'));
|
const complexityDisplay =
|
||||||
|
typeof subtask.complexity === 'number'
|
||||||
|
? getComplexityWithColor(subtask.complexity)
|
||||||
|
: '--';
|
||||||
|
subRow.push(chalk.gray(complexityDisplay));
|
||||||
}
|
}
|
||||||
|
|
||||||
table.push(subRow);
|
table.push(subRow);
|
||||||
|
|||||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.28.0-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.28.0-rc.1",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.5-rc.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "*"
|
"task-master-ai": "*"
|
||||||
},
|
},
|
||||||
@@ -635,7 +635,6 @@
|
|||||||
"apps/extension/node_modules/zod": {
|
"apps/extension/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -1830,7 +1829,6 @@
|
|||||||
"version": "7.28.4",
|
"version": "7.28.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.3",
|
"@babel/generator": "^7.28.3",
|
||||||
@@ -2663,7 +2661,6 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dnd-kit/accessibility": "^3.1.1",
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
@@ -4583,6 +4580,7 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5172,6 +5170,7 @@
|
|||||||
"version": "0.23.2",
|
"version": "0.23.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
@@ -5180,7 +5179,6 @@
|
|||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5471,7 +5469,6 @@
|
|||||||
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
"node_modules/@modelcontextprotocol/sdk/node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -5572,7 +5569,6 @@
|
|||||||
"node_modules/@opentelemetry/api": {
|
"node_modules/@opentelemetry/api": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
@@ -8592,7 +8588,6 @@
|
|||||||
"version": "19.1.8",
|
"version": "19.1.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -8601,7 +8596,6 @@
|
|||||||
"version": "19.1.6",
|
"version": "19.1.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -9047,7 +9041,6 @@
|
|||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -9113,7 +9106,6 @@
|
|||||||
"node_modules/ai": {
|
"node_modules/ai": {
|
||||||
"version": "5.0.57",
|
"version": "5.0.57",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/gateway": "1.0.30",
|
"@ai-sdk/gateway": "1.0.30",
|
||||||
"@ai-sdk/provider": "2.0.0",
|
"@ai-sdk/provider": "2.0.0",
|
||||||
@@ -9333,7 +9325,6 @@
|
|||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -10339,7 +10330,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.3",
|
"baseline-browser-mapping": "^2.8.3",
|
||||||
"caniuse-lite": "^1.0.30001741",
|
"caniuse-lite": "^1.0.30001741",
|
||||||
@@ -12203,8 +12193,7 @@
|
|||||||
"node_modules/devtools-protocol": {
|
"node_modules/devtools-protocol": {
|
||||||
"version": "0.0.1312386",
|
"version": "0.0.1312386",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/dezalgo": {
|
"node_modules/dezalgo": {
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
@@ -12798,7 +12787,6 @@
|
|||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
@@ -13111,7 +13099,6 @@
|
|||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.21.2",
|
"version": "4.21.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -15465,7 +15452,6 @@
|
|||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.2.0",
|
"@alcalzone/ansi-tokenize": "^0.2.0",
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
@@ -16423,7 +16409,6 @@
|
|||||||
"version": "29.7.0",
|
"version": "29.7.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "^29.7.0",
|
"@jest/core": "^29.7.0",
|
||||||
"@jest/types": "^29.6.3",
|
"@jest/types": "^29.6.3",
|
||||||
@@ -18041,7 +18026,6 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.16.0"
|
"node": ">= 10.16.0"
|
||||||
}
|
}
|
||||||
@@ -18367,6 +18351,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 12.0.0"
|
"node": ">= 12.0.0"
|
||||||
},
|
},
|
||||||
@@ -18591,6 +18576,7 @@
|
|||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
},
|
},
|
||||||
@@ -18721,7 +18707,6 @@
|
|||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "15.0.12",
|
"version": "15.0.12",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"marked": "bin/marked.js"
|
"marked": "bin/marked.js"
|
||||||
},
|
},
|
||||||
@@ -21444,7 +21429,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -22827,7 +22811,6 @@
|
|||||||
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
"integrity": "sha512-U+NPR0Bkg3wm61dteD2L4nAM1U9dtaqVrpDXwC36IKRHpEO/Ubpid4Nijpa2imPchcVNHfxVFwSSMJdwdGFUbg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oxc-project/types": "=0.93.0",
|
"@oxc-project/types": "=0.93.0",
|
||||||
"@rolldown/pluginutils": "1.0.0-beta.41",
|
"@rolldown/pluginutils": "1.0.0-beta.41",
|
||||||
@@ -25256,7 +25239,6 @@
|
|||||||
"version": "5.9.2",
|
"version": "5.9.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -25373,7 +25355,6 @@
|
|||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/unist": "^3.0.0",
|
"@types/unist": "^3.0.0",
|
||||||
"bail": "^2.0.0",
|
"bail": "^2.0.0",
|
||||||
@@ -25816,7 +25797,6 @@
|
|||||||
"version": "5.4.20",
|
"version": "5.4.20",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -25929,6 +25909,7 @@
|
|||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"darwin"
|
||||||
],
|
],
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
@@ -26512,7 +26493,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.2",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -26655,7 +26636,6 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.1.11",
|
"version": "4.1.11",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -27397,7 +27377,6 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/chai": "^5.2.2",
|
"@types/chai": "^5.2.2",
|
||||||
"@vitest/expect": "3.2.4",
|
"@vitest/expect": "3.2.4",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class TaskEntity implements Task {
|
|||||||
// Normalize subtask IDs to strings
|
// Normalize subtask IDs to strings
|
||||||
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
this.subtasks = (data.subtasks || []).map((subtask) => ({
|
||||||
...subtask,
|
...subtask,
|
||||||
id: Number(subtask.id), // Keep subtask IDs as numbers per interface
|
id: String(subtask.id),
|
||||||
parentId: String(subtask.parentId)
|
parentId: String(subtask.parentId)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
148
packages/tm-core/src/mappers/TaskMapper.test.ts
Normal file
148
packages/tm-core/src/mappers/TaskMapper.test.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { TaskMapper } from './TaskMapper.js';
|
||||||
|
import type { Tables } from '../types/database.types.js';
|
||||||
|
|
||||||
|
type TaskRow = Tables<'tasks'>;
|
||||||
|
|
||||||
|
describe('TaskMapper', () => {
|
||||||
|
describe('extractMetadataField', () => {
|
||||||
|
it('should extract string field from metadata', () => {
|
||||||
|
const taskRow: TaskRow = {
|
||||||
|
id: '123',
|
||||||
|
display_id: '1',
|
||||||
|
title: 'Test Task',
|
||||||
|
description: 'Test description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
parent_task_id: null,
|
||||||
|
subtask_position: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
metadata: {
|
||||||
|
details: 'Some details',
|
||||||
|
testStrategy: 'Test with unit tests'
|
||||||
|
},
|
||||||
|
complexity: null,
|
||||||
|
assignee_id: null,
|
||||||
|
estimated_hours: null,
|
||||||
|
actual_hours: null,
|
||||||
|
due_date: null,
|
||||||
|
completed_at: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||||
|
|
||||||
|
expect(task.details).toBe('Some details');
|
||||||
|
expect(task.testStrategy).toBe('Test with unit tests');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default value when metadata field is missing', () => {
|
||||||
|
const taskRow: TaskRow = {
|
||||||
|
id: '123',
|
||||||
|
display_id: '1',
|
||||||
|
title: 'Test Task',
|
||||||
|
description: 'Test description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
parent_task_id: null,
|
||||||
|
subtask_position: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
metadata: {},
|
||||||
|
complexity: null,
|
||||||
|
assignee_id: null,
|
||||||
|
estimated_hours: null,
|
||||||
|
actual_hours: null,
|
||||||
|
due_date: null,
|
||||||
|
completed_at: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||||
|
|
||||||
|
expect(task.details).toBe('');
|
||||||
|
expect(task.testStrategy).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default value when metadata is null', () => {
|
||||||
|
const taskRow: TaskRow = {
|
||||||
|
id: '123',
|
||||||
|
display_id: '1',
|
||||||
|
title: 'Test Task',
|
||||||
|
description: 'Test description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
parent_task_id: null,
|
||||||
|
subtask_position: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
metadata: null,
|
||||||
|
complexity: null,
|
||||||
|
assignee_id: null,
|
||||||
|
estimated_hours: null,
|
||||||
|
actual_hours: null,
|
||||||
|
due_date: null,
|
||||||
|
completed_at: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||||
|
|
||||||
|
expect(task.details).toBe('');
|
||||||
|
expect(task.testStrategy).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default value and warn when metadata field has wrong type', () => {
|
||||||
|
const consoleWarnSpy = vi
|
||||||
|
.spyOn(console, 'warn')
|
||||||
|
.mockImplementation(() => {});
|
||||||
|
|
||||||
|
const taskRow: TaskRow = {
|
||||||
|
id: '123',
|
||||||
|
display_id: '1',
|
||||||
|
title: 'Test Task',
|
||||||
|
description: 'Test description',
|
||||||
|
status: 'todo',
|
||||||
|
priority: 'medium',
|
||||||
|
parent_task_id: null,
|
||||||
|
subtask_position: 0,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
metadata: {
|
||||||
|
details: 12345, // Wrong type: number instead of string
|
||||||
|
testStrategy: ['test1', 'test2'] // Wrong type: array instead of string
|
||||||
|
},
|
||||||
|
complexity: null,
|
||||||
|
assignee_id: null,
|
||||||
|
estimated_hours: null,
|
||||||
|
actual_hours: null,
|
||||||
|
due_date: null,
|
||||||
|
completed_at: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const task = TaskMapper.mapDatabaseTaskToTask(taskRow, [], new Map());
|
||||||
|
|
||||||
|
// Should use empty string defaults when type doesn't match
|
||||||
|
expect(task.details).toBe('');
|
||||||
|
expect(task.testStrategy).toBe('');
|
||||||
|
|
||||||
|
// Should have logged warnings
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Type mismatch in metadata field "details"')
|
||||||
|
);
|
||||||
|
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
'Type mismatch in metadata field "testStrategy"'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
consoleWarnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapStatus', () => {
|
||||||
|
it('should map database status to internal status', () => {
|
||||||
|
expect(TaskMapper.mapStatus('todo')).toBe('pending');
|
||||||
|
expect(TaskMapper.mapStatus('in_progress')).toBe('in-progress');
|
||||||
|
expect(TaskMapper.mapStatus('done')).toBe('done');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,22 +2,32 @@ import { Task, Subtask } from '../types/index.js';
|
|||||||
import { Database, Tables } from '../types/database.types.js';
|
import { Database, Tables } from '../types/database.types.js';
|
||||||
|
|
||||||
type TaskRow = Tables<'tasks'>;
|
type TaskRow = Tables<'tasks'>;
|
||||||
type DependencyRow = Tables<'task_dependencies'>;
|
|
||||||
|
// Legacy type for backward compatibility
|
||||||
|
type DependencyRow = Tables<'task_dependencies'> & {
|
||||||
|
depends_on_task?: { display_id: string } | null;
|
||||||
|
depends_on_task_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class TaskMapper {
|
export class TaskMapper {
|
||||||
/**
|
/**
|
||||||
* Maps database tasks to internal Task format
|
* Maps database tasks to internal Task format
|
||||||
|
* @param dbTasks - Array of tasks from database
|
||||||
|
* @param dependencies - Either a Map of task_id to display_ids or legacy array format
|
||||||
*/
|
*/
|
||||||
static mapDatabaseTasksToTasks(
|
static mapDatabaseTasksToTasks(
|
||||||
dbTasks: TaskRow[],
|
dbTasks: TaskRow[],
|
||||||
dbDependencies: DependencyRow[]
|
dependencies: Map<string, string[]> | DependencyRow[]
|
||||||
): Task[] {
|
): Task[] {
|
||||||
if (!dbTasks || dbTasks.length === 0) {
|
if (!dbTasks || dbTasks.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Group dependencies by task_id
|
// Handle both Map and array formats for backward compatibility
|
||||||
const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies);
|
const dependenciesByTaskId =
|
||||||
|
dependencies instanceof Map
|
||||||
|
? dependencies
|
||||||
|
: this.groupDependenciesByTaskId(dependencies);
|
||||||
|
|
||||||
// Separate parent tasks and subtasks
|
// Separate parent tasks and subtasks
|
||||||
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
|
const parentTasks = dbTasks.filter((t) => !t.parent_task_id);
|
||||||
@@ -43,21 +53,23 @@ export class TaskMapper {
|
|||||||
): Task {
|
): Task {
|
||||||
// Map subtasks
|
// Map subtasks
|
||||||
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
|
const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({
|
||||||
id: index + 1, // Use numeric ID for subtasks
|
id: subtask.display_id || String(index + 1), // Use display_id if available (API storage), fallback to numeric (file storage)
|
||||||
parentId: dbTask.id,
|
parentId: dbTask.id,
|
||||||
title: subtask.title,
|
title: subtask.title,
|
||||||
description: subtask.description || '',
|
description: subtask.description || '',
|
||||||
status: this.mapStatus(subtask.status),
|
status: this.mapStatus(subtask.status),
|
||||||
priority: this.mapPriority(subtask.priority),
|
priority: this.mapPriority(subtask.priority),
|
||||||
dependencies: dependenciesByTaskId.get(subtask.id) || [],
|
dependencies: dependenciesByTaskId.get(subtask.id) || [],
|
||||||
details: (subtask.metadata as any)?.details || '',
|
details: this.extractMetadataField(subtask.metadata, 'details', ''),
|
||||||
testStrategy: (subtask.metadata as any)?.testStrategy || '',
|
testStrategy: this.extractMetadataField(
|
||||||
|
subtask.metadata,
|
||||||
|
'testStrategy',
|
||||||
|
''
|
||||||
|
),
|
||||||
createdAt: subtask.created_at,
|
createdAt: subtask.created_at,
|
||||||
updatedAt: subtask.updated_at,
|
updatedAt: subtask.updated_at,
|
||||||
assignee: subtask.assignee_id || undefined,
|
assignee: subtask.assignee_id || undefined,
|
||||||
complexity: subtask.complexity
|
complexity: subtask.complexity ?? undefined
|
||||||
? this.mapComplexityToInternal(subtask.complexity)
|
|
||||||
: undefined
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -67,22 +79,25 @@ export class TaskMapper {
|
|||||||
status: this.mapStatus(dbTask.status),
|
status: this.mapStatus(dbTask.status),
|
||||||
priority: this.mapPriority(dbTask.priority),
|
priority: this.mapPriority(dbTask.priority),
|
||||||
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
|
dependencies: dependenciesByTaskId.get(dbTask.id) || [],
|
||||||
details: (dbTask.metadata as any)?.details || '',
|
details: this.extractMetadataField(dbTask.metadata, 'details', ''),
|
||||||
testStrategy: (dbTask.metadata as any)?.testStrategy || '',
|
testStrategy: this.extractMetadataField(
|
||||||
|
dbTask.metadata,
|
||||||
|
'testStrategy',
|
||||||
|
''
|
||||||
|
),
|
||||||
subtasks,
|
subtasks,
|
||||||
createdAt: dbTask.created_at,
|
createdAt: dbTask.created_at,
|
||||||
updatedAt: dbTask.updated_at,
|
updatedAt: dbTask.updated_at,
|
||||||
assignee: dbTask.assignee_id || undefined,
|
assignee: dbTask.assignee_id || undefined,
|
||||||
complexity: dbTask.complexity
|
complexity: dbTask.complexity ?? undefined,
|
||||||
? this.mapComplexityToInternal(dbTask.complexity)
|
|
||||||
: undefined,
|
|
||||||
effort: dbTask.estimated_hours || undefined,
|
effort: dbTask.estimated_hours || undefined,
|
||||||
actualEffort: dbTask.actual_hours || undefined
|
actualEffort: dbTask.actual_hours || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups dependencies by task ID
|
* Groups dependencies by task ID (legacy method for backward compatibility)
|
||||||
|
* @deprecated Use DependencyFetcher.fetchDependenciesWithDisplayIds instead
|
||||||
*/
|
*/
|
||||||
private static groupDependenciesByTaskId(
|
private static groupDependenciesByTaskId(
|
||||||
dependencies: DependencyRow[]
|
dependencies: DependencyRow[]
|
||||||
@@ -92,7 +107,14 @@ export class TaskMapper {
|
|||||||
if (dependencies) {
|
if (dependencies) {
|
||||||
for (const dep of dependencies) {
|
for (const dep of dependencies) {
|
||||||
const deps = dependenciesByTaskId.get(dep.task_id) || [];
|
const deps = dependenciesByTaskId.get(dep.task_id) || [];
|
||||||
deps.push(dep.depends_on_task_id);
|
// Handle both old format (UUID string) and new format (object with display_id)
|
||||||
|
const dependencyId =
|
||||||
|
typeof dep.depends_on_task === 'object'
|
||||||
|
? dep.depends_on_task?.display_id
|
||||||
|
: dep.depends_on_task_id;
|
||||||
|
if (dependencyId) {
|
||||||
|
deps.push(dependencyId);
|
||||||
|
}
|
||||||
dependenciesByTaskId.set(dep.task_id, deps);
|
dependenciesByTaskId.set(dep.task_id, deps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,14 +179,38 @@ export class TaskMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps numeric complexity to descriptive complexity
|
* Safely extracts a field from metadata JSON with runtime type validation
|
||||||
|
* @param metadata The metadata object (could be null or any type)
|
||||||
|
* @param field The field to extract
|
||||||
|
* @param defaultValue Default value if field doesn't exist
|
||||||
|
* @returns The extracted value if it matches the expected type, otherwise defaultValue
|
||||||
*/
|
*/
|
||||||
private static mapComplexityToInternal(
|
private static extractMetadataField<T>(
|
||||||
complexity: number
|
metadata: unknown,
|
||||||
): Task['complexity'] {
|
field: string,
|
||||||
if (complexity <= 2) return 'simple';
|
defaultValue: T
|
||||||
if (complexity <= 5) return 'moderate';
|
): T {
|
||||||
if (complexity <= 8) return 'complex';
|
if (!metadata || typeof metadata !== 'object') {
|
||||||
return 'very-complex';
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = (metadata as Record<string, unknown>)[field];
|
||||||
|
|
||||||
|
if (value === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runtime type validation: ensure value matches the type of defaultValue
|
||||||
|
const expectedType = typeof defaultValue;
|
||||||
|
const actualType = typeof value;
|
||||||
|
|
||||||
|
if (expectedType !== actualType) {
|
||||||
|
console.warn(
|
||||||
|
`Type mismatch in metadata field "${field}": expected ${expectedType}, got ${actualType}. Using default value.`
|
||||||
|
);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value as T;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import { Database } from '../../types/database.types.js';
|
||||||
|
import { DependencyWithDisplayId } from '../../types/repository-types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles fetching and processing of task dependencies with display_ids
|
||||||
|
*/
|
||||||
|
export class DependencyFetcher {
|
||||||
|
constructor(private supabase: SupabaseClient<Database>) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches dependencies for given task IDs with display_ids joined
|
||||||
|
* @param taskIds Array of task IDs to fetch dependencies for
|
||||||
|
* @returns Map of task ID to array of dependency display_ids
|
||||||
|
*/
|
||||||
|
async fetchDependenciesWithDisplayIds(
|
||||||
|
taskIds: string[]
|
||||||
|
): Promise<Map<string, string[]>> {
|
||||||
|
if (!taskIds || taskIds.length === 0) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data, error } = await this.supabase
|
||||||
|
.from('task_dependencies')
|
||||||
|
.select(`
|
||||||
|
task_id,
|
||||||
|
depends_on_task:tasks!task_dependencies_depends_on_task_id_fkey (
|
||||||
|
display_id
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
.in('task_id', taskIds);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(`Failed to fetch task dependencies: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.processDependencyData(data as DependencyWithDisplayId[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes raw dependency data into a map structure
|
||||||
|
*/
|
||||||
|
private processDependencyData(
|
||||||
|
dependencies: DependencyWithDisplayId[]
|
||||||
|
): Map<string, string[]> {
|
||||||
|
const dependenciesByTaskId = new Map<string, string[]>();
|
||||||
|
|
||||||
|
if (!dependencies) {
|
||||||
|
return dependenciesByTaskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
if (!dep.task_id) continue;
|
||||||
|
|
||||||
|
const currentDeps = dependenciesByTaskId.get(dep.task_id) || [];
|
||||||
|
|
||||||
|
// Extract display_id from the joined object
|
||||||
|
const displayId = dep.depends_on_task?.display_id;
|
||||||
|
if (displayId) {
|
||||||
|
currentDeps.push(displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
dependenciesByTaskId.set(dep.task_id, currentDeps);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependenciesByTaskId;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
packages/tm-core/src/repositories/supabase/index.ts
Normal file
5
packages/tm-core/src/repositories/supabase/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Supabase repository implementations
|
||||||
|
*/
|
||||||
|
export { SupabaseTaskRepository } from './supabase-task-repository.js';
|
||||||
|
export { DependencyFetcher } from './dependency-fetcher.js';
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
import { Task } from '../types/index.js';
|
import { Task } from '../../types/index.js';
|
||||||
import { Database } from '../types/database.types.js';
|
import { Database, Json } from '../../types/database.types.js';
|
||||||
import { TaskMapper } from '../mappers/TaskMapper.js';
|
import { TaskMapper } from '../../mappers/TaskMapper.js';
|
||||||
import { AuthManager } from '../auth/auth-manager.js';
|
import { AuthManager } from '../../auth/auth-manager.js';
|
||||||
|
import { DependencyFetcher } from './dependency-fetcher.js';
|
||||||
|
import {
|
||||||
|
TaskWithRelations,
|
||||||
|
TaskDatabaseUpdate
|
||||||
|
} from '../../types/repository-types.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Zod schema for task status validation
|
// Zod schema for task status validation
|
||||||
@@ -29,18 +34,30 @@ const TaskUpdateSchema = z
|
|||||||
.partial();
|
.partial();
|
||||||
|
|
||||||
export class SupabaseTaskRepository {
|
export class SupabaseTaskRepository {
|
||||||
constructor(private supabase: SupabaseClient<Database>) {}
|
private dependencyFetcher: DependencyFetcher;
|
||||||
|
private authManager: AuthManager;
|
||||||
|
|
||||||
async getTasks(_projectId?: string): Promise<Task[]> {
|
constructor(private supabase: SupabaseClient<Database>) {
|
||||||
// Get the current context to determine briefId
|
this.dependencyFetcher = new DependencyFetcher(supabase);
|
||||||
const authManager = AuthManager.getInstance();
|
this.authManager = AuthManager.getInstance();
|
||||||
const context = authManager.getContext();
|
}
|
||||||
|
|
||||||
if (!context || !context.briefId) {
|
/**
|
||||||
|
* Gets the current brief ID from auth context
|
||||||
|
* @throws {Error} If no brief is selected
|
||||||
|
*/
|
||||||
|
private getBriefIdOrThrow(): string {
|
||||||
|
const context = this.authManager.getContext();
|
||||||
|
if (!context?.briefId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
'No brief selected. Please select a brief first using: tm context brief'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return context.briefId;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTasks(_projectId?: string): Promise<Task[]> {
|
||||||
|
const briefId = this.getBriefIdOrThrow();
|
||||||
|
|
||||||
// Get all tasks for the brief using the exact query structure
|
// Get all tasks for the brief using the exact query structure
|
||||||
const { data: tasks, error } = await this.supabase
|
const { data: tasks, error } = await this.supabase
|
||||||
@@ -54,7 +71,7 @@ export class SupabaseTaskRepository {
|
|||||||
description
|
description
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
.eq('brief_id', context.briefId)
|
.eq('brief_id', briefId)
|
||||||
.order('position', { ascending: true })
|
.order('position', { ascending: true })
|
||||||
.order('subtask_position', { ascending: true })
|
.order('subtask_position', { ascending: true })
|
||||||
.order('created_at', { ascending: true });
|
.order('created_at', { ascending: true });
|
||||||
@@ -67,38 +84,23 @@ export class SupabaseTaskRepository {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all dependencies for these tasks
|
// Type-safe task ID extraction
|
||||||
const taskIds = tasks.map((t: any) => t.id);
|
const typedTasks = tasks as TaskWithRelations[];
|
||||||
const { data: depsData, error: depsError } = await this.supabase
|
const taskIds = typedTasks.map((t) => t.id);
|
||||||
.from('task_dependencies')
|
const dependenciesMap =
|
||||||
.select('*')
|
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(taskIds);
|
||||||
.in('task_id', taskIds);
|
|
||||||
|
|
||||||
if (depsError) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to fetch task dependencies: ${depsError.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use mapper to convert to internal format
|
// Use mapper to convert to internal format
|
||||||
return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []);
|
return TaskMapper.mapDatabaseTasksToTasks(tasks, dependenciesMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
|
||||||
// Get the current context to determine briefId (projectId not used in Supabase context)
|
const briefId = this.getBriefIdOrThrow();
|
||||||
const authManager = AuthManager.getInstance();
|
|
||||||
const context = authManager.getContext();
|
|
||||||
|
|
||||||
if (!context || !context.briefId) {
|
|
||||||
throw new Error(
|
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('brief_id', context.briefId)
|
.eq('brief_id', briefId)
|
||||||
.eq('display_id', taskId.toUpperCase())
|
.eq('display_id', taskId.toUpperCase())
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
@@ -109,30 +111,19 @@ export class SupabaseTaskRepository {
|
|||||||
throw new Error(`Failed to fetch task: ${error.message}`);
|
throw new Error(`Failed to fetch task: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dependencies for this task
|
|
||||||
const { data: depsData } = await this.supabase
|
|
||||||
.from('task_dependencies')
|
|
||||||
.select('*')
|
|
||||||
.eq('task_id', taskId);
|
|
||||||
|
|
||||||
// Get subtasks if this is a parent task
|
// Get subtasks if this is a parent task
|
||||||
const { data: subtasksData } = await this.supabase
|
const { data: subtasksData } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('parent_task_id', taskId)
|
.eq('parent_task_id', data.id)
|
||||||
.order('subtask_position', { ascending: true });
|
.order('subtask_position', { ascending: true });
|
||||||
|
|
||||||
// Create dependency map
|
// Get all task IDs (parent + subtasks) to fetch dependencies
|
||||||
const dependenciesByTaskId = new Map<string, string[]>();
|
const allTaskIds = [data.id, ...(subtasksData?.map((st) => st.id) || [])];
|
||||||
if (depsData) {
|
|
||||||
dependenciesByTaskId.set(
|
// Fetch dependencies using the dedicated fetcher
|
||||||
taskId,
|
const dependenciesByTaskId =
|
||||||
depsData.map(
|
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(allTaskIds);
|
||||||
(d: Database['public']['Tables']['task_dependencies']['Row']) =>
|
|
||||||
d.depends_on_task_id
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use mapper to convert single task
|
// Use mapper to convert single task
|
||||||
return TaskMapper.mapDatabaseTaskToTask(
|
return TaskMapper.mapDatabaseTaskToTask(
|
||||||
@@ -147,15 +138,7 @@ export class SupabaseTaskRepository {
|
|||||||
taskId: string,
|
taskId: string,
|
||||||
updates: Partial<Task>
|
updates: Partial<Task>
|
||||||
): Promise<Task> {
|
): Promise<Task> {
|
||||||
// Get the current context to determine briefId
|
const briefId = this.getBriefIdOrThrow();
|
||||||
const authManager = AuthManager.getInstance();
|
|
||||||
const context = authManager.getContext();
|
|
||||||
|
|
||||||
if (!context || !context.briefId) {
|
|
||||||
throw new Error(
|
|
||||||
'No brief selected. Please select a brief first using: tm context brief'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate updates using Zod schema
|
// Validate updates using Zod schema
|
||||||
try {
|
try {
|
||||||
@@ -170,22 +153,50 @@ export class SupabaseTaskRepository {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert Task fields to database fields - only include fields that actually exist in the database
|
// Convert Task fields to database fields with proper typing
|
||||||
const dbUpdates: any = {};
|
const dbUpdates: TaskDatabaseUpdate = {};
|
||||||
|
|
||||||
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
if (updates.title !== undefined) dbUpdates.title = updates.title;
|
||||||
if (updates.description !== undefined)
|
if (updates.description !== undefined)
|
||||||
dbUpdates.description = updates.description;
|
dbUpdates.description = updates.description;
|
||||||
if (updates.status !== undefined)
|
if (updates.status !== undefined)
|
||||||
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
dbUpdates.status = this.mapStatusToDatabase(updates.status);
|
||||||
if (updates.priority !== undefined) dbUpdates.priority = updates.priority;
|
if (updates.priority !== undefined)
|
||||||
// Skip fields that don't exist in database schema: details, testStrategy, etc.
|
dbUpdates.priority = this.mapPriorityToDatabase(updates.priority);
|
||||||
|
|
||||||
|
// Handle metadata fields (details, testStrategy, etc.)
|
||||||
|
// Load existing metadata to preserve fields not being updated
|
||||||
|
const { data: existingMetadataRow, error: existingMetadataError } =
|
||||||
|
await this.supabase
|
||||||
|
.from('tasks')
|
||||||
|
.select('metadata')
|
||||||
|
.eq('brief_id', briefId)
|
||||||
|
.eq('display_id', taskId.toUpperCase())
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (existingMetadataError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to load existing task metadata: ${existingMetadataError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata: Record<string, unknown> = {
|
||||||
|
...((existingMetadataRow?.metadata as Record<string, unknown>) ?? {})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updates.details !== undefined) metadata.details = updates.details;
|
||||||
|
if (updates.testStrategy !== undefined)
|
||||||
|
metadata.testStrategy = updates.testStrategy;
|
||||||
|
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
dbUpdates.metadata = metadata as Json;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the task
|
// Update the task
|
||||||
const { error } = await this.supabase
|
const { error } = await this.supabase
|
||||||
.from('tasks')
|
.from('tasks')
|
||||||
.update(dbUpdates)
|
.update(dbUpdates)
|
||||||
.eq('brief_id', context.briefId)
|
.eq('brief_id', briefId)
|
||||||
.eq('display_id', taskId.toUpperCase());
|
.eq('display_id', taskId.toUpperCase());
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
@@ -221,4 +232,25 @@ export class SupabaseTaskRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps internal priority to database priority
|
||||||
|
* Task Master uses 'critical', database uses 'urgent'
|
||||||
|
*/
|
||||||
|
private mapPriorityToDatabase(
|
||||||
|
priority: string
|
||||||
|
): Database['public']['Enums']['task_priority'] {
|
||||||
|
switch (priority) {
|
||||||
|
case 'critical':
|
||||||
|
return 'urgent';
|
||||||
|
case 'low':
|
||||||
|
case 'medium':
|
||||||
|
case 'high':
|
||||||
|
return priority as Database['public']['Enums']['task_priority'];
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Invalid task priority: ${priority}. Valid priorities are: low, medium, high, critical`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,7 @@ import type {
|
|||||||
} from '../types/index.js';
|
} from '../types/index.js';
|
||||||
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
|
||||||
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
import { TaskRepository } from '../repositories/task-repository.interface.js';
|
||||||
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
|
import { SupabaseTaskRepository } from '../repositories/supabase/index.js';
|
||||||
import { SupabaseClient } from '@supabase/supabase-js';
|
import { SupabaseClient } from '@supabase/supabase-js';
|
||||||
import { AuthManager } from '../auth/auth-manager.js';
|
import { AuthManager } from '../auth/auth-manager.js';
|
||||||
|
|
||||||
|
|||||||
@@ -82,10 +82,11 @@ export interface Task {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subtask interface extending Task with numeric ID
|
* Subtask interface extending Task
|
||||||
|
* ID can be number (file storage) or string (API storage with display_id)
|
||||||
*/
|
*/
|
||||||
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
export interface Subtask extends Omit<Task, 'id' | 'subtasks'> {
|
||||||
id: number;
|
id: number | string;
|
||||||
parentId: string;
|
parentId: string;
|
||||||
subtasks?: never; // Subtasks cannot have their own subtasks
|
subtasks?: never; // Subtasks cannot have their own subtasks
|
||||||
}
|
}
|
||||||
|
|||||||
83
packages/tm-core/src/types/repository-types.ts
Normal file
83
packages/tm-core/src/types/repository-types.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for repository operations
|
||||||
|
*/
|
||||||
|
import { Database, Tables } from './database.types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task row from database with optional joined relations
|
||||||
|
*/
|
||||||
|
export interface TaskWithRelations extends Tables<'tasks'> {
|
||||||
|
document?: {
|
||||||
|
id: string;
|
||||||
|
document_name: string;
|
||||||
|
title: string;
|
||||||
|
description: string | null;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dependency row with joined display_id
|
||||||
|
*/
|
||||||
|
export interface DependencyWithDisplayId {
|
||||||
|
task_id: string;
|
||||||
|
depends_on_task: {
|
||||||
|
display_id: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task metadata structure
|
||||||
|
*/
|
||||||
|
export interface TaskMetadata {
|
||||||
|
details?: string;
|
||||||
|
testStrategy?: string;
|
||||||
|
[key: string]: unknown; // Allow additional fields but be explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database update payload for tasks
|
||||||
|
*/
|
||||||
|
export type TaskDatabaseUpdate =
|
||||||
|
Database['public']['Tables']['tasks']['Update'];
|
||||||
|
/**
|
||||||
|
* Configuration for task queries
|
||||||
|
*/
|
||||||
|
export interface TaskQueryConfig {
|
||||||
|
briefId: string;
|
||||||
|
includeSubtasks?: boolean;
|
||||||
|
includeDependencies?: boolean;
|
||||||
|
includeDocument?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result of a task fetch operation
|
||||||
|
*/
|
||||||
|
export interface TaskFetchResult {
|
||||||
|
task: Tables<'tasks'>;
|
||||||
|
subtasks: Tables<'tasks'>[];
|
||||||
|
dependencies: Map<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Task validation errors
|
||||||
|
*/
|
||||||
|
export class TaskValidationError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public readonly field: string,
|
||||||
|
public readonly value: unknown
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'TaskValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context validation errors
|
||||||
|
*/
|
||||||
|
export class ContextValidationError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ContextValidationError';
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user