@hasna/todos 0.11.88 → 0.11.89
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cloud-router.d.ts +10 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +950 -118
- package/dist/contracts.js +301 -72
- package/dist/db/projects.d.ts +2 -2
- package/dist/db/projects.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/slug-claims.d.ts +11 -0
- package/dist/db/slug-claims.d.ts.map +1 -0
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/index.js +641 -100
- package/dist/lib/slugs.d.ts +19 -0
- package/dist/lib/slugs.d.ts.map +1 -0
- package/dist/mcp/index.js +897 -102
- package/dist/registry.js +301 -72
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/index.js +56 -0
- package/dist/sdk/v1.generated.d.ts +79 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +2 -0
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +906 -108
- package/dist/server/openapi.d.ts +464 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +3 -2
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts +30 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage/shadow.d.ts.map +1 -1
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +635 -88
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -1749,6 +1749,95 @@ function ensureSchema(db) {
|
|
|
1749
1749
|
)`);
|
|
1750
1750
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_project ON project_machine_paths(project_id)");
|
|
1751
1751
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_machine_paths_machine ON project_machine_paths(machine_id)");
|
|
1752
|
+
ensureTable("canonical_slug_claims", `
|
|
1753
|
+
CREATE TABLE canonical_slug_claims (
|
|
1754
|
+
kind TEXT NOT NULL CHECK(kind IN ('project', 'task_list')),
|
|
1755
|
+
scope_key TEXT NOT NULL,
|
|
1756
|
+
slug TEXT NOT NULL,
|
|
1757
|
+
object_id TEXT NOT NULL,
|
|
1758
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1759
|
+
PRIMARY KEY (kind, scope_key, slug)
|
|
1760
|
+
)`);
|
|
1761
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_canonical_slug_claims_object ON canonical_slug_claims(kind, object_id)");
|
|
1762
|
+
ensureColumn("projects", "task_list_id", "TEXT");
|
|
1763
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_project_canonical_slug_insert
|
|
1764
|
+
BEFORE INSERT ON projects
|
|
1765
|
+
WHEN NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> ''
|
|
1766
|
+
BEGIN
|
|
1767
|
+
SELECT CASE WHEN EXISTS (
|
|
1768
|
+
SELECT 1 FROM projects WHERE id <> NEW.id AND task_list_id = NEW.task_list_id
|
|
1769
|
+
) THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1770
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1771
|
+
VALUES ('project', 'global', NEW.task_list_id, NEW.id);
|
|
1772
|
+
SELECT CASE WHEN (
|
|
1773
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1774
|
+
WHERE kind = 'project' AND scope_key = 'global' AND slug = NEW.task_list_id
|
|
1775
|
+
) <> NEW.id THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1776
|
+
END`);
|
|
1777
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_project_canonical_slug_update
|
|
1778
|
+
BEFORE UPDATE OF task_list_id ON projects
|
|
1779
|
+
WHEN NEW.task_list_id IS NOT OLD.task_list_id
|
|
1780
|
+
BEGIN
|
|
1781
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'project' AND object_id = NEW.id;
|
|
1782
|
+
SELECT CASE WHEN EXISTS (
|
|
1783
|
+
SELECT 1 FROM projects WHERE id <> NEW.id AND task_list_id = NEW.task_list_id
|
|
1784
|
+
) AND NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> ''
|
|
1785
|
+
THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1786
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1787
|
+
SELECT 'project', 'global', NEW.task_list_id, NEW.id
|
|
1788
|
+
WHERE NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> '';
|
|
1789
|
+
SELECT CASE WHEN NEW.task_list_id IS NOT NULL AND NEW.task_list_id <> '' AND (
|
|
1790
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1791
|
+
WHERE kind = 'project' AND scope_key = 'global' AND slug = NEW.task_list_id
|
|
1792
|
+
) <> NEW.id THEN RAISE(ABORT, 'PROJECT_SLUG_CONFLICT') END;
|
|
1793
|
+
END`);
|
|
1794
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS release_project_canonical_slug_delete
|
|
1795
|
+
AFTER DELETE ON projects
|
|
1796
|
+
BEGIN
|
|
1797
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'project' AND object_id = OLD.id;
|
|
1798
|
+
END`);
|
|
1799
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_task_list_canonical_slug_insert
|
|
1800
|
+
BEFORE INSERT ON task_lists
|
|
1801
|
+
WHEN NEW.slug IS NOT NULL AND NEW.slug <> ''
|
|
1802
|
+
BEGIN
|
|
1803
|
+
SELECT CASE WHEN EXISTS (
|
|
1804
|
+
SELECT 1 FROM task_lists
|
|
1805
|
+
WHERE id <> NEW.id AND project_id IS NEW.project_id AND slug = NEW.slug
|
|
1806
|
+
) THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1807
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1808
|
+
VALUES ('task_list', CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END, NEW.slug, NEW.id);
|
|
1809
|
+
SELECT CASE WHEN (
|
|
1810
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1811
|
+
WHERE kind = 'task_list'
|
|
1812
|
+
AND scope_key = CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END
|
|
1813
|
+
AND slug = NEW.slug
|
|
1814
|
+
) <> NEW.id THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1815
|
+
END`);
|
|
1816
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS claim_task_list_canonical_slug_update
|
|
1817
|
+
BEFORE UPDATE OF slug, project_id ON task_lists
|
|
1818
|
+
WHEN NEW.slug IS NOT OLD.slug OR NEW.project_id IS NOT OLD.project_id
|
|
1819
|
+
BEGIN
|
|
1820
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'task_list' AND object_id = NEW.id;
|
|
1821
|
+
SELECT CASE WHEN EXISTS (
|
|
1822
|
+
SELECT 1 FROM task_lists
|
|
1823
|
+
WHERE id <> NEW.id AND project_id IS NEW.project_id AND slug = NEW.slug
|
|
1824
|
+
) AND NEW.slug IS NOT NULL AND NEW.slug <> ''
|
|
1825
|
+
THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1826
|
+
INSERT OR IGNORE INTO canonical_slug_claims(kind, scope_key, slug, object_id)
|
|
1827
|
+
SELECT 'task_list', CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END, NEW.slug, NEW.id
|
|
1828
|
+
WHERE NEW.slug IS NOT NULL AND NEW.slug <> '';
|
|
1829
|
+
SELECT CASE WHEN NEW.slug IS NOT NULL AND NEW.slug <> '' AND (
|
|
1830
|
+
SELECT object_id FROM canonical_slug_claims
|
|
1831
|
+
WHERE kind = 'task_list'
|
|
1832
|
+
AND scope_key = CASE WHEN NEW.project_id IS NULL THEN 'standalone:' ELSE 'project:' || NEW.project_id END
|
|
1833
|
+
AND slug = NEW.slug
|
|
1834
|
+
) <> NEW.id THEN RAISE(ABORT, 'TASK_LIST_SLUG_CONFLICT') END;
|
|
1835
|
+
END`);
|
|
1836
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS release_task_list_canonical_slug_delete
|
|
1837
|
+
AFTER DELETE ON task_lists
|
|
1838
|
+
BEGIN
|
|
1839
|
+
DELETE FROM canonical_slug_claims WHERE kind = 'task_list' AND object_id = OLD.id;
|
|
1840
|
+
END`);
|
|
1752
1841
|
ensureTable("storage_tombstones", `
|
|
1753
1842
|
CREATE TABLE storage_tombstones (
|
|
1754
1843
|
id TEXT PRIMARY KEY,
|
|
@@ -3898,7 +3987,7 @@ var init_agents = __esm(() => {
|
|
|
3898
3987
|
async function logError(_message, _opts) {}
|
|
3899
3988
|
|
|
3900
3989
|
// src/types/index.ts
|
|
3901
|
-
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
3990
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
3902
3991
|
var init_types = __esm(() => {
|
|
3903
3992
|
TASK_STATUSES = [
|
|
3904
3993
|
"pending",
|
|
@@ -3941,6 +4030,14 @@ var init_types = __esm(() => {
|
|
|
3941
4030
|
this.name = "ProjectNotFoundError";
|
|
3942
4031
|
}
|
|
3943
4032
|
};
|
|
4033
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
4034
|
+
code;
|
|
4035
|
+
constructor(code, message) {
|
|
4036
|
+
super(message);
|
|
4037
|
+
this.code = code;
|
|
4038
|
+
this.name = "ResourceConflictError";
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
3944
4041
|
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3945
4042
|
planId;
|
|
3946
4043
|
static code = "PLAN_NOT_FOUND";
|
|
@@ -8188,6 +8285,91 @@ var init_config2 = __esm(() => {
|
|
|
8188
8285
|
};
|
|
8189
8286
|
});
|
|
8190
8287
|
|
|
8288
|
+
// src/lib/slugs.ts
|
|
8289
|
+
function normalizeSlug(value) {
|
|
8290
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
8291
|
+
}
|
|
8292
|
+
function isCanonicalSlug(value) {
|
|
8293
|
+
return typeof value === "string" && value.length > 0 && normalizeSlug(value) === value;
|
|
8294
|
+
}
|
|
8295
|
+
function isValidTaskListProjectScope(value) {
|
|
8296
|
+
return value === undefined || value === null || typeof value === "string" && value.trim().length > 0;
|
|
8297
|
+
}
|
|
8298
|
+
function validateSnapshotRoutingRecords(projects, taskLists) {
|
|
8299
|
+
const errors2 = [];
|
|
8300
|
+
const projectSlugs = new Map;
|
|
8301
|
+
const taskListSlugs = new Map;
|
|
8302
|
+
for (const project of projects) {
|
|
8303
|
+
if (!isCanonicalSlug(project.task_list_id)) {
|
|
8304
|
+
errors2.push(`project ${project.id}: task_list_id must be non-empty canonical kebab-case`);
|
|
8305
|
+
continue;
|
|
8306
|
+
}
|
|
8307
|
+
const existing = projectSlugs.get(project.task_list_id);
|
|
8308
|
+
if (projectSlugs.has(project.task_list_id)) {
|
|
8309
|
+
errors2.push(`project ${project.id}: task_list_id duplicates project ${existing}: ${project.task_list_id}`);
|
|
8310
|
+
} else {
|
|
8311
|
+
projectSlugs.set(project.task_list_id, project.id);
|
|
8312
|
+
}
|
|
8313
|
+
}
|
|
8314
|
+
for (const taskList of taskLists) {
|
|
8315
|
+
if (!isCanonicalSlug(taskList.slug)) {
|
|
8316
|
+
errors2.push(`task list ${taskList.id}: slug must be non-empty canonical kebab-case`);
|
|
8317
|
+
continue;
|
|
8318
|
+
}
|
|
8319
|
+
if (!isValidTaskListProjectScope(taskList.project_id)) {
|
|
8320
|
+
errors2.push(`task list ${taskList.id}: project_id must be null, missing, or a non-empty string`);
|
|
8321
|
+
continue;
|
|
8322
|
+
}
|
|
8323
|
+
const scope = taskList.project_id ?? null;
|
|
8324
|
+
const scopedSlugs = taskListSlugs.get(scope) ?? new Map;
|
|
8325
|
+
const existing = scopedSlugs.get(taskList.slug);
|
|
8326
|
+
if (scopedSlugs.has(taskList.slug)) {
|
|
8327
|
+
errors2.push(`task list ${taskList.id}: slug duplicates task list ${existing} in the same scope: ${taskList.slug}`);
|
|
8328
|
+
} else {
|
|
8329
|
+
scopedSlugs.set(taskList.slug, taskList.id);
|
|
8330
|
+
taskListSlugs.set(scope, scopedSlugs);
|
|
8331
|
+
}
|
|
8332
|
+
}
|
|
8333
|
+
return errors2;
|
|
8334
|
+
}
|
|
8335
|
+
function validateSnapshotRoutingDestinationConflicts(projects, taskLists, existingProjects, existingTaskLists) {
|
|
8336
|
+
const errors2 = [];
|
|
8337
|
+
for (const project of projects) {
|
|
8338
|
+
const current = existingProjects.find((candidate) => candidate.id === project.id);
|
|
8339
|
+
if (current?.task_list_id === project.task_list_id)
|
|
8340
|
+
continue;
|
|
8341
|
+
const conflict = existingProjects.find((candidate) => candidate.id !== project.id && candidate.task_list_id === project.task_list_id);
|
|
8342
|
+
if (conflict) {
|
|
8343
|
+
errors2.push(`project ${project.id}: task_list_id conflicts with existing project ${conflict.id}: ${String(project.task_list_id)}`);
|
|
8344
|
+
}
|
|
8345
|
+
}
|
|
8346
|
+
for (const taskList of taskLists) {
|
|
8347
|
+
const projectId = taskList.project_id ?? null;
|
|
8348
|
+
const current = existingTaskLists.find((candidate) => candidate.id === taskList.id);
|
|
8349
|
+
if ((current?.project_id ?? null) === projectId && current?.slug === taskList.slug)
|
|
8350
|
+
continue;
|
|
8351
|
+
const conflict = existingTaskLists.find((candidate) => candidate.id !== taskList.id && (candidate.project_id ?? null) === projectId && candidate.slug === taskList.slug);
|
|
8352
|
+
if (conflict) {
|
|
8353
|
+
errors2.push(`task list ${taskList.id}: slug conflicts with existing task list ${conflict.id} in the same scope: ${String(taskList.slug)}`);
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
return errors2;
|
|
8357
|
+
}
|
|
8358
|
+
|
|
8359
|
+
// src/db/slug-claims.ts
|
|
8360
|
+
function taskListSlugScopeKey(projectId) {
|
|
8361
|
+
return projectId ? `project:${projectId}` : "standalone:";
|
|
8362
|
+
}
|
|
8363
|
+
function claimCanonicalSlug(kind, scopeKey, slug, objectId, db) {
|
|
8364
|
+
db.run(`INSERT OR IGNORE INTO canonical_slug_claims (kind, scope_key, slug, object_id)
|
|
8365
|
+
VALUES (?, ?, ?, ?)`, [kind, scopeKey, slug, objectId]);
|
|
8366
|
+
const claim = db.query("SELECT object_id FROM canonical_slug_claims WHERE kind = ? AND scope_key = ? AND slug = ?").get(kind, scopeKey, slug);
|
|
8367
|
+
return claim?.object_id === objectId;
|
|
8368
|
+
}
|
|
8369
|
+
function releaseCanonicalSlugClaims(kind, objectId, db) {
|
|
8370
|
+
db.run("DELETE FROM canonical_slug_claims WHERE kind = ? AND object_id = ?", [kind, objectId]);
|
|
8371
|
+
}
|
|
8372
|
+
|
|
8191
8373
|
// src/db/projects.ts
|
|
8192
8374
|
var exports_projects = {};
|
|
8193
8375
|
__export(exports_projects, {
|
|
@@ -8211,7 +8393,7 @@ __export(exports_projects, {
|
|
|
8211
8393
|
addProjectSource: () => addProjectSource
|
|
8212
8394
|
});
|
|
8213
8395
|
function slugify(name) {
|
|
8214
|
-
return name
|
|
8396
|
+
return normalizeSlug(name);
|
|
8215
8397
|
}
|
|
8216
8398
|
function generatePrefix(name, db) {
|
|
8217
8399
|
const words = name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/);
|
|
@@ -8235,14 +8417,23 @@ function generatePrefix(name, db) {
|
|
|
8235
8417
|
}
|
|
8236
8418
|
function createProject(input, db) {
|
|
8237
8419
|
const d = db || getDatabase();
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
8420
|
+
return d.transaction(() => {
|
|
8421
|
+
const id = uuid();
|
|
8422
|
+
const timestamp = now();
|
|
8423
|
+
const derivedSlug = slugify(input.name);
|
|
8424
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugify(input.task_list_id);
|
|
8425
|
+
if (!derivedSlug || !taskListId)
|
|
8426
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
8427
|
+
const slugConflict = d.query("SELECT id FROM projects WHERE task_list_id = ? LIMIT 1").get(taskListId);
|
|
8428
|
+
if (slugConflict || !claimCanonicalSlug("project", "global", taskListId, id, d)) {
|
|
8429
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${taskListId}" already exists`);
|
|
8430
|
+
}
|
|
8431
|
+
const taskPrefix = input.task_prefix || generatePrefix(input.name, d);
|
|
8432
|
+
const machineId = currentStorageMachineId(d);
|
|
8433
|
+
d.run(`INSERT INTO projects (id, name, path, description, task_list_id, task_prefix, task_counter, created_at, updated_at, machine_id)
|
|
8434
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`, [id, input.name, input.path, input.description || null, taskListId, taskPrefix, timestamp, timestamp, machineId]);
|
|
8435
|
+
return getProject(id, d);
|
|
8436
|
+
})();
|
|
8246
8437
|
}
|
|
8247
8438
|
function getProject(id, db) {
|
|
8248
8439
|
const d = db || getDatabase();
|
|
@@ -8270,6 +8461,9 @@ function updateProject(id, input, db) {
|
|
|
8270
8461
|
const project = getProject(id, d);
|
|
8271
8462
|
if (!project)
|
|
8272
8463
|
throw new ProjectNotFoundError(id);
|
|
8464
|
+
if ("task_list_id" in input) {
|
|
8465
|
+
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
8466
|
+
}
|
|
8273
8467
|
const sets = ["updated_at = ?"];
|
|
8274
8468
|
const params = [now()];
|
|
8275
8469
|
if (input.name !== undefined) {
|
|
@@ -8280,10 +8474,6 @@ function updateProject(id, input, db) {
|
|
|
8280
8474
|
sets.push("description = ?");
|
|
8281
8475
|
params.push(input.description);
|
|
8282
8476
|
}
|
|
8283
|
-
if (input.task_list_id !== undefined) {
|
|
8284
|
-
sets.push("task_list_id = ?");
|
|
8285
|
-
params.push(input.task_list_id);
|
|
8286
|
-
}
|
|
8287
8477
|
if (input.path !== undefined) {
|
|
8288
8478
|
sets.push("path = ?");
|
|
8289
8479
|
params.push(input.path);
|
|
@@ -8294,29 +8484,41 @@ function updateProject(id, input, db) {
|
|
|
8294
8484
|
}
|
|
8295
8485
|
function renameProject(id, input, db) {
|
|
8296
8486
|
const d = db || getDatabase();
|
|
8297
|
-
|
|
8298
|
-
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
|
|
8312
|
-
|
|
8313
|
-
|
|
8487
|
+
return d.transaction(() => {
|
|
8488
|
+
const project = getProject(id, d);
|
|
8489
|
+
if (!project)
|
|
8490
|
+
throw new ProjectNotFoundError(id);
|
|
8491
|
+
let taskListsUpdated = 0;
|
|
8492
|
+
const ts = now();
|
|
8493
|
+
if (input.new_slug !== undefined) {
|
|
8494
|
+
const normalised = normalizeSlug(input.new_slug);
|
|
8495
|
+
if (!normalised)
|
|
8496
|
+
throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
|
|
8497
|
+
const oldSlug = project.task_list_id;
|
|
8498
|
+
if (normalised !== oldSlug) {
|
|
8499
|
+
const conflict = d.query("SELECT id FROM projects WHERE task_list_id = ? AND id != ?").get(normalised, id);
|
|
8500
|
+
if (conflict) {
|
|
8501
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalised}" is already used by another project`);
|
|
8502
|
+
}
|
|
8503
|
+
const taskListConflict = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND slug != COALESCE(?, '') LIMIT 1").get(id, normalised, oldSlug);
|
|
8504
|
+
if (taskListConflict) {
|
|
8505
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task-list slug "${normalised}" is already used in project "${project.name}"`);
|
|
8506
|
+
}
|
|
8507
|
+
releaseCanonicalSlugClaims("project", id, d);
|
|
8508
|
+
if (!claimCanonicalSlug("project", "global", normalised, id, d)) {
|
|
8509
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalised}" is already used by another project`);
|
|
8510
|
+
}
|
|
8511
|
+
d.run("UPDATE projects SET task_list_id = ?, updated_at = ? WHERE id = ?", [normalised, ts, id]);
|
|
8512
|
+
}
|
|
8513
|
+
if (oldSlug && (normalised !== oldSlug || input.name !== undefined && input.name !== project.name)) {
|
|
8514
|
+
taskListsUpdated = d.query("UPDATE task_lists SET slug = ?, name = COALESCE(?, name), updated_at = ? WHERE project_id = ? AND slug = ? RETURNING id").all(normalised, input.name ?? null, ts, id, oldSlug).length;
|
|
8515
|
+
}
|
|
8314
8516
|
}
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8517
|
+
if (input.name !== undefined && input.name !== project.name) {
|
|
8518
|
+
d.run("UPDATE projects SET name = ?, updated_at = ? WHERE id = ?", [input.name, ts, id]);
|
|
8519
|
+
}
|
|
8520
|
+
return { project: getProject(id, d), task_lists_updated: taskListsUpdated };
|
|
8521
|
+
})();
|
|
8320
8522
|
}
|
|
8321
8523
|
function deleteProject(id, db) {
|
|
8322
8524
|
const d = db || getDatabase();
|
|
@@ -8328,8 +8530,10 @@ function deleteProject(id, db) {
|
|
|
8328
8530
|
object_id: id,
|
|
8329
8531
|
payload: project
|
|
8330
8532
|
}, d);
|
|
8331
|
-
|
|
8332
|
-
|
|
8533
|
+
return d.transaction(() => {
|
|
8534
|
+
releaseCanonicalSlugClaims("project", id, d);
|
|
8535
|
+
return d.run("DELETE FROM projects WHERE id = ?", [id]).changes > 0;
|
|
8536
|
+
})();
|
|
8333
8537
|
}
|
|
8334
8538
|
function rowToSource(row) {
|
|
8335
8539
|
return {
|
|
@@ -9882,19 +10086,22 @@ function rowToTaskList(row) {
|
|
|
9882
10086
|
}
|
|
9883
10087
|
function createTaskList(input, db) {
|
|
9884
10088
|
const d = db || getDatabase();
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
9888
|
-
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
10089
|
+
return d.transaction(() => {
|
|
10090
|
+
const id = uuid();
|
|
10091
|
+
const timestamp = now();
|
|
10092
|
+
const slug = normalizeSlug(input.slug === undefined ? input.name : input.slug);
|
|
10093
|
+
if (!slug)
|
|
10094
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
10095
|
+
const machineId = currentStorageMachineId(d);
|
|
10096
|
+
const scopeKey = taskListSlugScopeKey(input.project_id);
|
|
10097
|
+
const existing = input.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(input.project_id, slug) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
10098
|
+
if (existing || !claimCanonicalSlug("task_list", scopeKey, slug, id, d)) {
|
|
10099
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
10100
|
+
}
|
|
10101
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at, machine_id)
|
|
10102
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp, machineId]);
|
|
10103
|
+
return getTaskList(id, d);
|
|
10104
|
+
})();
|
|
9898
10105
|
}
|
|
9899
10106
|
function getTaskList(id, db) {
|
|
9900
10107
|
const d = db || getDatabase();
|
|
@@ -9920,26 +10127,45 @@ function listTaskLists(projectId, db) {
|
|
|
9920
10127
|
}
|
|
9921
10128
|
function updateTaskList(id, input, db) {
|
|
9922
10129
|
const d = db || getDatabase();
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
10130
|
+
return d.transaction(() => {
|
|
10131
|
+
const existing = getTaskList(id, d);
|
|
10132
|
+
if (!existing)
|
|
10133
|
+
throw new TaskListNotFoundError(id);
|
|
10134
|
+
const sets = ["updated_at = ?"];
|
|
10135
|
+
const params = [now()];
|
|
10136
|
+
if (input.slug !== undefined) {
|
|
10137
|
+
const slug = slugify(input.slug);
|
|
10138
|
+
if (!slug)
|
|
10139
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
10140
|
+
const duplicate = existing.project_id ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(existing.project_id, slug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(slug, id);
|
|
10141
|
+
if (duplicate) {
|
|
10142
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
10143
|
+
}
|
|
10144
|
+
if (slug !== existing.slug) {
|
|
10145
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
10146
|
+
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(existing.project_id), slug, id, d)) {
|
|
10147
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
10148
|
+
}
|
|
10149
|
+
}
|
|
10150
|
+
sets.push("slug = ?");
|
|
10151
|
+
params.push(slug);
|
|
10152
|
+
}
|
|
10153
|
+
if (input.name !== undefined) {
|
|
10154
|
+
sets.push("name = ?");
|
|
10155
|
+
params.push(input.name);
|
|
10156
|
+
}
|
|
10157
|
+
if (input.description !== undefined) {
|
|
10158
|
+
sets.push("description = ?");
|
|
10159
|
+
params.push(input.description);
|
|
10160
|
+
}
|
|
10161
|
+
if (input.metadata !== undefined) {
|
|
10162
|
+
sets.push("metadata = ?");
|
|
10163
|
+
params.push(JSON.stringify(input.metadata));
|
|
10164
|
+
}
|
|
10165
|
+
params.push(id);
|
|
10166
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
10167
|
+
return getTaskList(id, d);
|
|
10168
|
+
})();
|
|
9943
10169
|
}
|
|
9944
10170
|
function deleteTaskList(id, db) {
|
|
9945
10171
|
const d = db || getDatabase();
|
|
@@ -9951,7 +10177,10 @@ function deleteTaskList(id, db) {
|
|
|
9951
10177
|
object_id: id,
|
|
9952
10178
|
payload: list
|
|
9953
10179
|
}, d);
|
|
9954
|
-
return d.
|
|
10180
|
+
return d.transaction(() => {
|
|
10181
|
+
releaseCanonicalSlugClaims("task_list", id, d);
|
|
10182
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
10183
|
+
})();
|
|
9955
10184
|
}
|
|
9956
10185
|
function ensureTaskList(name, slug, projectId, db) {
|
|
9957
10186
|
const d = db || getDatabase();
|
|
@@ -23352,10 +23581,10 @@ function bootstrapProject(options = {}, db) {
|
|
|
23352
23581
|
let project = ensureProject(options.name || discovery.projectName, discovery.projectPath, d);
|
|
23353
23582
|
const createdProject = !beforeProject;
|
|
23354
23583
|
if (project.task_list_id !== taskListSlug || options.name && project.name !== options.name) {
|
|
23355
|
-
project =
|
|
23584
|
+
project = renameProject(project.id, {
|
|
23356
23585
|
name: options.name ?? project.name,
|
|
23357
|
-
|
|
23358
|
-
}, d);
|
|
23586
|
+
new_slug: taskListSlug
|
|
23587
|
+
}, d).project;
|
|
23359
23588
|
}
|
|
23360
23589
|
setMachineLocalPath(project.id, discovery.projectPath, d);
|
|
23361
23590
|
const beforeTaskList = d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ?").get(project.id, taskListSlug);
|
|
@@ -45809,6 +46038,14 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45809
46038
|
skipped: 0,
|
|
45810
46039
|
errors: []
|
|
45811
46040
|
};
|
|
46041
|
+
result.errors.push(...validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists));
|
|
46042
|
+
if (result.errors.length === 0) {
|
|
46043
|
+
const existingProjects = d.query("SELECT id, task_list_id FROM projects").all();
|
|
46044
|
+
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
46045
|
+
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
46046
|
+
}
|
|
46047
|
+
if (result.errors.length > 0)
|
|
46048
|
+
return result;
|
|
45812
46049
|
const applyRows = (objectType3, table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
45813
46050
|
for (const row of rows) {
|
|
45814
46051
|
try {
|
|
@@ -46181,6 +46418,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
46181
46418
|
getByPath: (path) => getProjectByPath(path, database()),
|
|
46182
46419
|
list: () => listProjects(database()),
|
|
46183
46420
|
update: (id, input) => updateProject(id, input, database()),
|
|
46421
|
+
rename: (id, input) => renameProject(id, input, database()),
|
|
46184
46422
|
delete: (id) => deleteProject(id, database())
|
|
46185
46423
|
},
|
|
46186
46424
|
plans: {
|
|
@@ -46288,6 +46526,91 @@ function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABL
|
|
|
46288
46526
|
)`
|
|
46289
46527
|
];
|
|
46290
46528
|
}
|
|
46529
|
+
function postgresTodosScopedSlugPreflightSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46530
|
+
assertSafeIdentifier(tableName);
|
|
46531
|
+
return `/* todos:scoped-slug-duplicate-audit */ WITH candidates AS (
|
|
46532
|
+
SELECT service, object_type, COALESCE(payload->>'project_id', '') AS scope,
|
|
46533
|
+
jsonb_typeof(payload->'project_id') AS scope_type,
|
|
46534
|
+
payload->>'slug' AS slug, jsonb_typeof(payload->'slug') AS slug_type, object_id
|
|
46535
|
+
FROM ${tableName}
|
|
46536
|
+
WHERE object_type = 'task_lists' AND deleted_at IS NULL
|
|
46537
|
+
UNION ALL
|
|
46538
|
+
SELECT service, object_type, '' AS scope, NULL::text AS scope_type, payload->>'task_list_id' AS slug,
|
|
46539
|
+
jsonb_typeof(payload->'task_list_id') AS slug_type, object_id
|
|
46540
|
+
FROM ${tableName}
|
|
46541
|
+
WHERE object_type = 'projects' AND deleted_at IS NULL
|
|
46542
|
+
), annotated AS (
|
|
46543
|
+
SELECT *, trim(both '-' from regexp_replace(lower(COALESCE(slug, '')), '[^a-z0-9]+', '-', 'g')) AS normalized_slug
|
|
46544
|
+
FROM candidates
|
|
46545
|
+
), invalid AS (
|
|
46546
|
+
SELECT service, object_type, scope, COALESCE(slug, '<null>') AS slug,
|
|
46547
|
+
ARRAY[object_id] AS object_ids, 1::integer AS duplicate_count, 'invalid'::text AS issue
|
|
46548
|
+
FROM annotated
|
|
46549
|
+
WHERE slug_type IS DISTINCT FROM 'string'
|
|
46550
|
+
OR slug IS NULL OR slug = '' OR normalized_slug = '' OR slug IS DISTINCT FROM normalized_slug
|
|
46551
|
+
OR (object_type = 'task_lists' AND (
|
|
46552
|
+
(scope_type IS NOT NULL AND scope_type NOT IN ('string', 'null'))
|
|
46553
|
+
OR (scope_type = 'string' AND btrim(scope) = '')
|
|
46554
|
+
))
|
|
46555
|
+
), duplicates AS (
|
|
46556
|
+
SELECT service, object_type, scope, slug,
|
|
46557
|
+
array_agg(object_id ORDER BY object_id) AS object_ids,
|
|
46558
|
+
count(*)::integer AS duplicate_count, 'duplicate'::text AS issue
|
|
46559
|
+
FROM annotated
|
|
46560
|
+
WHERE slug = normalized_slug AND slug <> ''
|
|
46561
|
+
GROUP BY service, object_type, scope, slug
|
|
46562
|
+
HAVING count(*) > 1
|
|
46563
|
+
) SELECT * FROM invalid
|
|
46564
|
+
UNION ALL SELECT * FROM duplicates
|
|
46565
|
+
ORDER BY service, object_type, scope, slug`;
|
|
46566
|
+
}
|
|
46567
|
+
function postgresTodosScopedSlugUniqueIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46568
|
+
assertSafeIdentifier(tableName);
|
|
46569
|
+
return [
|
|
46570
|
+
`CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_task_list_scope_slug_uidx
|
|
46571
|
+
ON ${tableName} (service, COALESCE(payload->>'project_id', ''), (payload->>'slug'))
|
|
46572
|
+
WHERE object_type = 'task_lists' AND deleted_at IS NULL AND COALESCE(payload->>'slug', '') <> ''`,
|
|
46573
|
+
`CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_project_task_list_slug_uidx
|
|
46574
|
+
ON ${tableName} (service, (payload->>'task_list_id'))
|
|
46575
|
+
WHERE object_type = 'projects' AND deleted_at IS NULL AND COALESCE(payload->>'task_list_id', '') <> ''`
|
|
46576
|
+
];
|
|
46577
|
+
}
|
|
46578
|
+
function postgresTodosScopedSlugIndexStatusSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46579
|
+
assertSafeIdentifier(tableName);
|
|
46580
|
+
return `/* todos:scoped-slug-index-status */ SELECT index_class.relname AS index_name,
|
|
46581
|
+
index_meta.indisvalid AS is_valid, index_meta.indisready AS is_ready
|
|
46582
|
+
FROM pg_index index_meta
|
|
46583
|
+
JOIN pg_class index_class ON index_class.oid = index_meta.indexrelid
|
|
46584
|
+
WHERE index_meta.indrelid = to_regclass('${tableName}')
|
|
46585
|
+
AND index_class.relname IN (
|
|
46586
|
+
'${tableName}_task_list_scope_slug_uidx',
|
|
46587
|
+
'${tableName}_project_task_list_slug_uidx'
|
|
46588
|
+
)`;
|
|
46589
|
+
}
|
|
46590
|
+
async function ensurePostgresScopedSlugUniqueIndexes(client, tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46591
|
+
const audit = await client.query(postgresTodosScopedSlugPreflightSql(tableName));
|
|
46592
|
+
if (audit.rows.length > 0)
|
|
46593
|
+
throw new PostgresScopedSlugMigrationConflictError(audit.rows);
|
|
46594
|
+
for (const sql of postgresTodosScopedSlugUniqueIndexSql(tableName)) {
|
|
46595
|
+
try {
|
|
46596
|
+
await client.query(sql);
|
|
46597
|
+
} catch (error) {
|
|
46598
|
+
const indexName = sql.match(/INDEX CONCURRENTLY IF NOT EXISTS ([a-zA-Z0-9_]+)/)?.[1] ?? "unknown_index";
|
|
46599
|
+
throw new PostgresScopedSlugIndexBuildError(indexName, error);
|
|
46600
|
+
}
|
|
46601
|
+
}
|
|
46602
|
+
const expected = new Set([
|
|
46603
|
+
`${tableName}_task_list_scope_slug_uidx`,
|
|
46604
|
+
`${tableName}_project_task_list_slug_uidx`
|
|
46605
|
+
]);
|
|
46606
|
+
const status = await client.query(postgresTodosScopedSlugIndexStatusSql(tableName));
|
|
46607
|
+
for (const indexName of expected) {
|
|
46608
|
+
const row = status.rows.find((candidate) => candidate.index_name === indexName);
|
|
46609
|
+
if (!row?.is_valid || !row.is_ready) {
|
|
46610
|
+
throw new PostgresScopedSlugIndexBuildError(indexName, new Error("index is missing, invalid, or not ready"));
|
|
46611
|
+
}
|
|
46612
|
+
}
|
|
46613
|
+
}
|
|
46291
46614
|
function postgresTodosCommentCursorIndexSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE) {
|
|
46292
46615
|
assertSafeIdentifier(tableName);
|
|
46293
46616
|
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${tableName}_comment_task_created_idx
|
|
@@ -46316,9 +46639,31 @@ class PostgresTodosSyncStore {
|
|
|
46316
46639
|
}
|
|
46317
46640
|
}
|
|
46318
46641
|
async pushSnapshot(snapshot, context = {}) {
|
|
46642
|
+
const routingErrors = validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists);
|
|
46643
|
+
if (routingErrors.length > 0) {
|
|
46644
|
+
throw new Error(`Invalid snapshot routing metadata: ${routingErrors.join("; ")}`);
|
|
46645
|
+
}
|
|
46646
|
+
const existing = await this.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
46647
|
+
FROM ${this.tableName}
|
|
46648
|
+
WHERE service = $1 AND object_type IN ($2, $3) AND deleted_at IS NULL`, [this.service, "projects", "task_lists"]);
|
|
46649
|
+
const existingProjects = [];
|
|
46650
|
+
const existingTaskLists = [];
|
|
46651
|
+
for (const row of existing.rows) {
|
|
46652
|
+
const payload = payloadRecord(row.payload);
|
|
46653
|
+
if (row.object_type === "projects")
|
|
46654
|
+
existingProjects.push(payload);
|
|
46655
|
+
if (row.object_type === "task_lists")
|
|
46656
|
+
existingTaskLists.push(payload);
|
|
46657
|
+
}
|
|
46658
|
+
const destinationErrors = validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists);
|
|
46659
|
+
if (destinationErrors.length > 0) {
|
|
46660
|
+
throw new Error(`Snapshot routing conflicts with destination: ${destinationErrors.join("; ")}`);
|
|
46661
|
+
}
|
|
46319
46662
|
const result = { records: 0, objectTypes: {} };
|
|
46320
46663
|
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
46321
46664
|
for (const entry of snapshotEntries(snapshot)) {
|
|
46665
|
+
if (entry.deletedAt === null)
|
|
46666
|
+
assertCanonicalScopedSlugEntry(entry);
|
|
46322
46667
|
await this.client.query(`INSERT INTO ${this.tableName} (
|
|
46323
46668
|
service, object_type, object_id, payload, updated_at,
|
|
46324
46669
|
deleted_at, source_machine_id, version
|
|
@@ -46397,6 +46742,22 @@ function snapshotEntries(snapshot) {
|
|
|
46397
46742
|
}))
|
|
46398
46743
|
];
|
|
46399
46744
|
}
|
|
46745
|
+
function assertCanonicalScopedSlugEntry(entry) {
|
|
46746
|
+
if (!entry.payload || typeof entry.payload !== "object" || Array.isArray(entry.payload))
|
|
46747
|
+
return;
|
|
46748
|
+
const payload = entry.payload;
|
|
46749
|
+
if (entry.type === "projects" && !isCanonicalSlug(payload["task_list_id"])) {
|
|
46750
|
+
throw new Error("Invalid project task-list slug \u2014 sync requires non-empty canonical kebab-case");
|
|
46751
|
+
}
|
|
46752
|
+
if (entry.type === "task_lists") {
|
|
46753
|
+
if (!isCanonicalSlug(payload["slug"])) {
|
|
46754
|
+
throw new Error("Invalid task-list slug \u2014 sync requires non-empty canonical kebab-case");
|
|
46755
|
+
}
|
|
46756
|
+
if (!isValidTaskListProjectScope(payload["project_id"])) {
|
|
46757
|
+
throw new Error("Invalid task-list project scope \u2014 project_id must be null, missing, or a non-empty string");
|
|
46758
|
+
}
|
|
46759
|
+
}
|
|
46760
|
+
}
|
|
46400
46761
|
function entry(type, payload, fallbackUpdatedAt) {
|
|
46401
46762
|
const id = payload["id"];
|
|
46402
46763
|
if (typeof id !== "string" || !id)
|
|
@@ -46479,7 +46840,26 @@ function assertSafeIdentifier(value) {
|
|
|
46479
46840
|
if (!/^[a-z_][a-z0-9_]*$/i.test(value))
|
|
46480
46841
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
46481
46842
|
}
|
|
46482
|
-
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
46843
|
+
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records", DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors", PostgresScopedSlugMigrationConflictError, PostgresScopedSlugIndexBuildError;
|
|
46844
|
+
var init_postgres_sync = __esm(() => {
|
|
46845
|
+
PostgresScopedSlugMigrationConflictError = class PostgresScopedSlugMigrationConflictError extends Error {
|
|
46846
|
+
conflicts;
|
|
46847
|
+
constructor(conflicts) {
|
|
46848
|
+
const preview = conflicts.slice(0, 5).map((conflict) => `${conflict.object_type}:${conflict.scope || "global"}:${conflict.slug} [${conflict.object_ids.join(", ")}]`).join("; ");
|
|
46849
|
+
super(`Scoped slug unique-index preflight found ${conflicts.length} invalid or duplicate slug conflict(s): ${preview}. ` + "Resolve these records explicitly without deleting history, then rerun todos-serve migrate.");
|
|
46850
|
+
this.conflicts = conflicts;
|
|
46851
|
+
this.name = "PostgresScopedSlugMigrationConflictError";
|
|
46852
|
+
}
|
|
46853
|
+
};
|
|
46854
|
+
PostgresScopedSlugIndexBuildError = class PostgresScopedSlugIndexBuildError extends Error {
|
|
46855
|
+
index_name;
|
|
46856
|
+
constructor(index_name, cause) {
|
|
46857
|
+
super(`Concurrent scoped-slug index build failed for ${index_name} after a clean duplicate audit. ` + "No records were rewritten; inspect pg_index for an invalid index, resolve any concurrent duplicate, and rerun todos-serve migrate.", { cause });
|
|
46858
|
+
this.index_name = index_name;
|
|
46859
|
+
this.name = "PostgresScopedSlugIndexBuildError";
|
|
46860
|
+
}
|
|
46861
|
+
};
|
|
46862
|
+
});
|
|
46483
46863
|
|
|
46484
46864
|
// src/storage/shadow-outbox.ts
|
|
46485
46865
|
class TodosShadowOutbox {
|
|
@@ -46726,6 +47106,7 @@ function emptySnapshot() {
|
|
|
46726
47106
|
var MAX_BACKOFF_MS;
|
|
46727
47107
|
var init_shadow_outbox = __esm(() => {
|
|
46728
47108
|
init_local_sqlite();
|
|
47109
|
+
init_postgres_sync();
|
|
46729
47110
|
init_shadow_outbox_schema();
|
|
46730
47111
|
init_shadow_outbox_schema();
|
|
46731
47112
|
MAX_BACKOFF_MS = 5 * 60000;
|
|
@@ -48238,6 +48619,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
48238
48619
|
getByPath: async (path) => (await store.list("projects")).find((project) => project.path === path) ?? null,
|
|
48239
48620
|
list: async () => (await store.list("projects")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
48240
48621
|
update: (id, input) => updateProject2(id, input, store),
|
|
48622
|
+
rename: (id, input, context) => store.renameProject(id, input.new_slug, input.name, context),
|
|
48241
48623
|
delete: (id, context) => store.delete("projects", id, context)
|
|
48242
48624
|
},
|
|
48243
48625
|
plans: {
|
|
@@ -48475,9 +48857,22 @@ class PostgresJsonRecordStore {
|
|
|
48475
48857
|
});
|
|
48476
48858
|
}
|
|
48477
48859
|
async upsert(type, value, context = {}) {
|
|
48860
|
+
if (type === "projects" && !isCanonicalSlug(value.task_list_id)) {
|
|
48861
|
+
throw new Error("Invalid project task-list slug \u2014 imports require non-empty canonical kebab-case");
|
|
48862
|
+
}
|
|
48863
|
+
if (type === "task_lists") {
|
|
48864
|
+
if (!isCanonicalSlug(value.slug)) {
|
|
48865
|
+
throw new Error("Invalid task-list slug \u2014 imports require non-empty canonical kebab-case");
|
|
48866
|
+
}
|
|
48867
|
+
if (!isValidTaskListProjectScope(value.project_id)) {
|
|
48868
|
+
throw new Error("Invalid task-list project scope \u2014 project_id must be null, missing, or a non-empty string");
|
|
48869
|
+
}
|
|
48870
|
+
}
|
|
48478
48871
|
await this.ensureSchema();
|
|
48479
48872
|
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
48480
|
-
|
|
48873
|
+
let result;
|
|
48874
|
+
try {
|
|
48875
|
+
result = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
48481
48876
|
service, object_type, object_id, payload, updated_at,
|
|
48482
48877
|
deleted_at, source_machine_id, version
|
|
48483
48878
|
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
|
|
@@ -48492,14 +48887,23 @@ class PostgresJsonRecordStore {
|
|
|
48492
48887
|
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
48493
48888
|
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
48494
48889
|
RETURNING object_id`, [
|
|
48495
|
-
|
|
48496
|
-
|
|
48497
|
-
|
|
48498
|
-
|
|
48499
|
-
|
|
48500
|
-
|
|
48501
|
-
|
|
48502
|
-
|
|
48890
|
+
this.service,
|
|
48891
|
+
type,
|
|
48892
|
+
value.id,
|
|
48893
|
+
jsonbParam(value),
|
|
48894
|
+
updatedAt,
|
|
48895
|
+
context.requestId ?? this.sourceMachineId ?? null,
|
|
48896
|
+
numberValue3(value.version)
|
|
48897
|
+
]);
|
|
48898
|
+
} catch (error) {
|
|
48899
|
+
if (type === "task_lists" && isPostgresUniqueViolation(error)) {
|
|
48900
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${String(value.slug ?? "")}" already exists in this scope`);
|
|
48901
|
+
}
|
|
48902
|
+
if (type === "projects" && isPostgresUniqueViolation(error)) {
|
|
48903
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Project slug "${String(value.task_list_id ?? "")}" already exists`);
|
|
48904
|
+
}
|
|
48905
|
+
throw error;
|
|
48906
|
+
}
|
|
48503
48907
|
if (result.rows.length === 0) {
|
|
48504
48908
|
const current = await this.get(type, value.id);
|
|
48505
48909
|
if (current)
|
|
@@ -48507,6 +48911,93 @@ class PostgresJsonRecordStore {
|
|
|
48507
48911
|
}
|
|
48508
48912
|
return value;
|
|
48509
48913
|
}
|
|
48914
|
+
async renameProject(id, newSlug, name, context = {}) {
|
|
48915
|
+
await this.ensureSchema();
|
|
48916
|
+
const normalizedSlug = slugifyRaw(newSlug);
|
|
48917
|
+
if (!normalizedSlug)
|
|
48918
|
+
throw new Error("Invalid slug \u2014 must be non-empty kebab-case");
|
|
48919
|
+
const timestamp3 = new Date().toISOString();
|
|
48920
|
+
try {
|
|
48921
|
+
const result = await this.options.client.query(`/* todos:rename-project-atomic */ WITH target AS (
|
|
48922
|
+
SELECT payload, payload->>'task_list_id' AS old_slug
|
|
48923
|
+
FROM ${this.tableName}
|
|
48924
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL
|
|
48925
|
+
FOR UPDATE
|
|
48926
|
+
), project_conflict AS (
|
|
48927
|
+
SELECT 1 FROM ${this.tableName}
|
|
48928
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
48929
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3 LIMIT 1
|
|
48930
|
+
), task_list_conflict AS (
|
|
48931
|
+
SELECT 1 FROM ${this.tableName} r, target
|
|
48932
|
+
WHERE r.service = $1 AND r.object_type = 'task_lists' AND r.deleted_at IS NULL
|
|
48933
|
+
AND r.payload->>'project_id' = $2 AND r.payload->>'slug' = $3
|
|
48934
|
+
AND r.payload->>'slug' IS DISTINCT FROM target.old_slug LIMIT 1
|
|
48935
|
+
), updated_lists AS (
|
|
48936
|
+
UPDATE ${this.tableName} r SET
|
|
48937
|
+
payload = r.payload || jsonb_build_object('slug', $3::text, 'updated_at', $5::text)
|
|
48938
|
+
|| CASE WHEN $4::text IS NULL THEN '{}'::jsonb ELSE jsonb_build_object('name', $4::text) END,
|
|
48939
|
+
updated_at = $5::timestamptz, version = COALESCE(r.version, 0) + 1,
|
|
48940
|
+
source_machine_id = COALESCE($6, r.source_machine_id)
|
|
48941
|
+
FROM target
|
|
48942
|
+
WHERE r.service = $1 AND r.object_type = 'task_lists' AND r.deleted_at IS NULL
|
|
48943
|
+
AND r.payload->>'project_id' = $2 AND r.payload->>'slug' = target.old_slug
|
|
48944
|
+
AND NOT EXISTS (SELECT 1 FROM project_conflict)
|
|
48945
|
+
AND NOT EXISTS (SELECT 1 FROM task_list_conflict)
|
|
48946
|
+
AND (target.old_slug IS DISTINCT FROM $3
|
|
48947
|
+
OR ($4::text IS NOT NULL AND r.payload->>'name' IS DISTINCT FROM $4))
|
|
48948
|
+
RETURNING 1
|
|
48949
|
+
), updated_project AS (
|
|
48950
|
+
UPDATE ${this.tableName} r SET
|
|
48951
|
+
payload = r.payload || jsonb_build_object('task_list_id', $3::text, 'updated_at', $5::text)
|
|
48952
|
+
|| CASE WHEN $4::text IS NULL THEN '{}'::jsonb ELSE jsonb_build_object('name', $4::text) END,
|
|
48953
|
+
updated_at = $5::timestamptz, version = COALESCE(r.version, 0) + 1,
|
|
48954
|
+
source_machine_id = COALESCE($6, r.source_machine_id)
|
|
48955
|
+
FROM target
|
|
48956
|
+
WHERE r.service = $1 AND r.object_type = 'projects' AND r.object_id = $2 AND r.deleted_at IS NULL
|
|
48957
|
+
AND NOT EXISTS (SELECT 1 FROM project_conflict)
|
|
48958
|
+
AND NOT EXISTS (SELECT 1 FROM task_list_conflict)
|
|
48959
|
+
AND (target.old_slug IS DISTINCT FROM $3
|
|
48960
|
+
OR ($4::text IS NOT NULL AND target.payload->>'name' IS DISTINCT FROM $4))
|
|
48961
|
+
RETURNING r.payload
|
|
48962
|
+
) SELECT
|
|
48963
|
+
EXISTS (SELECT 1 FROM target) AS found,
|
|
48964
|
+
EXISTS (SELECT 1 FROM project_conflict) AS project_conflict,
|
|
48965
|
+
EXISTS (SELECT 1 FROM task_list_conflict) AS task_list_conflict,
|
|
48966
|
+
COALESCE((SELECT payload FROM updated_project), (SELECT payload FROM target)) AS project,
|
|
48967
|
+
(SELECT count(*) FROM updated_lists) AS task_lists_updated`, [this.service, id, normalizedSlug, name ?? null, timestamp3, this.machineId(context)]);
|
|
48968
|
+
const row = result.rows[0];
|
|
48969
|
+
if (!row?.found)
|
|
48970
|
+
throw new ProjectNotFoundError(id);
|
|
48971
|
+
if (row.project_conflict) {
|
|
48972
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalizedSlug}" is already used by another project`);
|
|
48973
|
+
}
|
|
48974
|
+
if (row.task_list_conflict) {
|
|
48975
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task-list slug "${normalizedSlug}" is already used in this project`);
|
|
48976
|
+
}
|
|
48977
|
+
return {
|
|
48978
|
+
project: payloadRecord2(row.project),
|
|
48979
|
+
task_lists_updated: Number(row.task_lists_updated)
|
|
48980
|
+
};
|
|
48981
|
+
} catch (error) {
|
|
48982
|
+
if (isPostgresUniqueViolation(error)) {
|
|
48983
|
+
const constraintName = postgresConstraintName(error);
|
|
48984
|
+
let projectConflict = constraintName.includes("project_task_list_slug_uidx");
|
|
48985
|
+
if (!constraintName) {
|
|
48986
|
+
const conflict = await this.options.client.query(`/* todos:classify-project-rename-conflict */ SELECT EXISTS (
|
|
48987
|
+
SELECT 1 FROM ${this.tableName}
|
|
48988
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id <> $2
|
|
48989
|
+
AND deleted_at IS NULL AND payload->>'task_list_id' = $3
|
|
48990
|
+
) AS project_conflict`, [this.service, id, normalizedSlug]);
|
|
48991
|
+
projectConflict = Boolean(conflict.rows[0]?.project_conflict);
|
|
48992
|
+
}
|
|
48993
|
+
if (projectConflict) {
|
|
48994
|
+
throw new ResourceConflictError("PROJECT_SLUG_CONFLICT", `Slug "${normalizedSlug}" is already used by another project`);
|
|
48995
|
+
}
|
|
48996
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task-list slug "${normalizedSlug}" is already used in this project`);
|
|
48997
|
+
}
|
|
48998
|
+
throw error;
|
|
48999
|
+
}
|
|
49000
|
+
}
|
|
48510
49001
|
async incrementProjectTaskCounter(projectId, _context = {}) {
|
|
48511
49002
|
await this.ensureSchema();
|
|
48512
49003
|
const result = await this.options.client.query(`UPDATE ${this.tableName}
|
|
@@ -48926,12 +49417,16 @@ async function getChangedSince(since, filters, store) {
|
|
|
48926
49417
|
}
|
|
48927
49418
|
async function createProject2(input, store, context) {
|
|
48928
49419
|
const timestamp3 = new Date().toISOString();
|
|
49420
|
+
const derivedSlug = slugifyRaw(input.name);
|
|
49421
|
+
const taskListId = input.task_list_id === undefined ? `todos-${derivedSlug}` : slugifyRaw(input.task_list_id);
|
|
49422
|
+
if (!derivedSlug || !taskListId)
|
|
49423
|
+
throw new Error("Project name and task-list slug must be non-empty");
|
|
48929
49424
|
const project = {
|
|
48930
49425
|
id: randomUUID3(),
|
|
48931
49426
|
name: input.name,
|
|
48932
49427
|
path: input.path,
|
|
48933
49428
|
description: input.description ?? null,
|
|
48934
|
-
task_list_id:
|
|
49429
|
+
task_list_id: taskListId,
|
|
48935
49430
|
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
48936
49431
|
task_counter: 0,
|
|
48937
49432
|
created_at: timestamp3,
|
|
@@ -48942,6 +49437,9 @@ async function createProject2(input, store, context) {
|
|
|
48942
49437
|
return store.upsert("projects", project, context);
|
|
48943
49438
|
}
|
|
48944
49439
|
async function updateProject2(id, input, store) {
|
|
49440
|
+
if ("task_list_id" in input) {
|
|
49441
|
+
throw new Error("task_list_id cannot be changed by updateProject; use renameProject for an atomic canonical rename");
|
|
49442
|
+
}
|
|
48945
49443
|
const project = await requireRecord("projects", id, store);
|
|
48946
49444
|
const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
|
|
48947
49445
|
return store.upsert("projects", updated);
|
|
@@ -49050,10 +49548,13 @@ async function releaseAgent2(idOrName, sessionId, store, context) {
|
|
|
49050
49548
|
}
|
|
49051
49549
|
async function createTaskList2(input, store, context) {
|
|
49052
49550
|
const timestamp3 = new Date().toISOString();
|
|
49551
|
+
const slug = slugifyRaw(input.slug === undefined ? input.name : input.slug);
|
|
49552
|
+
if (!slug)
|
|
49553
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
49053
49554
|
return store.upsert("task_lists", {
|
|
49054
49555
|
id: randomUUID3(),
|
|
49055
49556
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
49056
|
-
slug
|
|
49557
|
+
slug,
|
|
49057
49558
|
name: input.name,
|
|
49058
49559
|
description: input.description ?? null,
|
|
49059
49560
|
metadata: input.metadata ?? {},
|
|
@@ -49065,9 +49566,20 @@ async function createTaskList2(input, store, context) {
|
|
|
49065
49566
|
}
|
|
49066
49567
|
async function updateTaskList2(id, input, store) {
|
|
49067
49568
|
const list = await requireRecord("task_lists", id, store);
|
|
49569
|
+
const patch = definedPatch(input);
|
|
49570
|
+
if (input.slug !== undefined) {
|
|
49571
|
+
const slug = slugifyRaw(input.slug);
|
|
49572
|
+
if (!slug)
|
|
49573
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
49574
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === list.project_id && candidate.slug === slug);
|
|
49575
|
+
if (duplicate) {
|
|
49576
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${slug}" already exists in this scope`);
|
|
49577
|
+
}
|
|
49578
|
+
patch.slug = slug;
|
|
49579
|
+
}
|
|
49068
49580
|
return store.upsert("task_lists", {
|
|
49069
49581
|
...list,
|
|
49070
|
-
...
|
|
49582
|
+
...patch,
|
|
49071
49583
|
metadata: input.metadata ?? list.metadata,
|
|
49072
49584
|
updated_at: new Date().toISOString()
|
|
49073
49585
|
});
|
|
@@ -49151,6 +49663,16 @@ async function exportSnapshot(store) {
|
|
|
49151
49663
|
}
|
|
49152
49664
|
async function importSnapshot(snapshot, store, context) {
|
|
49153
49665
|
const result = { inserted: 0, updated: 0, deleted: 0, skipped: 0, errors: [] };
|
|
49666
|
+
result.errors.push(...validateSnapshotRoutingRecords(snapshot.projects, snapshot.taskLists));
|
|
49667
|
+
if (result.errors.length > 0)
|
|
49668
|
+
return result;
|
|
49669
|
+
const [existingProjects, existingTaskLists] = await Promise.all([
|
|
49670
|
+
store.list("projects"),
|
|
49671
|
+
store.list("task_lists")
|
|
49672
|
+
]);
|
|
49673
|
+
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
49674
|
+
if (result.errors.length > 0)
|
|
49675
|
+
return result;
|
|
49154
49676
|
const entries = [
|
|
49155
49677
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
49156
49678
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -49221,10 +49743,7 @@ async function generateProjectPrefix(name, store) {
|
|
|
49221
49743
|
return candidate;
|
|
49222
49744
|
}
|
|
49223
49745
|
function slugifyRaw(value) {
|
|
49224
|
-
return value
|
|
49225
|
-
}
|
|
49226
|
-
function slugify2(value) {
|
|
49227
|
-
return slugifyRaw(value) || "todos";
|
|
49746
|
+
return normalizeSlug(value);
|
|
49228
49747
|
}
|
|
49229
49748
|
function normalizePlanSlug2(value) {
|
|
49230
49749
|
const slug = slugifyRaw(value);
|
|
@@ -49278,9 +49797,20 @@ function compareClock(left, right) {
|
|
|
49278
49797
|
function numberValue3(value) {
|
|
49279
49798
|
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
49280
49799
|
}
|
|
49800
|
+
function isPostgresUniqueViolation(error) {
|
|
49801
|
+
return typeof error === "object" && error !== null && error.code === "23505";
|
|
49802
|
+
}
|
|
49803
|
+
function postgresConstraintName(error) {
|
|
49804
|
+
if (typeof error !== "object" || error === null)
|
|
49805
|
+
return "";
|
|
49806
|
+
const candidate = error;
|
|
49807
|
+
const constraint = candidate.constraint ?? candidate.constraint_name;
|
|
49808
|
+
return typeof constraint === "string" ? constraint : "";
|
|
49809
|
+
}
|
|
49281
49810
|
var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_BY = "ORDER BY CASE payload->>'priority' WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 ELSE 4 END ASC, payload->>'created_at' ASC, payload->>'id' ASC";
|
|
49282
49811
|
var init_postgres_adapter = __esm(() => {
|
|
49283
49812
|
init_types();
|
|
49813
|
+
init_postgres_sync();
|
|
49284
49814
|
init_redaction();
|
|
49285
49815
|
});
|
|
49286
49816
|
|
|
@@ -49378,6 +49908,7 @@ async function backfillPostgresCommentRedaction(client, options = {}) {
|
|
|
49378
49908
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
49379
49909
|
var init_comment_redaction_backfill = __esm(() => {
|
|
49380
49910
|
init_redaction();
|
|
49911
|
+
init_postgres_sync();
|
|
49381
49912
|
});
|
|
49382
49913
|
|
|
49383
49914
|
// src/server/cloud.ts
|
|
@@ -49391,6 +49922,7 @@ __export(exports_cloud, {
|
|
|
49391
49922
|
getCloudVerifier: () => getCloudVerifier,
|
|
49392
49923
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
49393
49924
|
getApiKeyStore: () => getApiKeyStore,
|
|
49925
|
+
ensureCloudScopedSlugUniqueIndexes: () => ensureCloudScopedSlugUniqueIndexes,
|
|
49394
49926
|
ensureCloudSchema: () => ensureCloudSchema,
|
|
49395
49927
|
ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
|
|
49396
49928
|
closeCloud: () => closeCloud,
|
|
@@ -49475,6 +50007,9 @@ async function ensureCloudSchema() {
|
|
|
49475
50007
|
async function ensureCloudCommentCursorIndex() {
|
|
49476
50008
|
await getClient().query(postgresTodosCommentCursorIndexSql());
|
|
49477
50009
|
}
|
|
50010
|
+
async function ensureCloudScopedSlugUniqueIndexes() {
|
|
50011
|
+
await ensurePostgresScopedSlugUniqueIndexes(getClient());
|
|
50012
|
+
}
|
|
49478
50013
|
async function normalizeCloudPayloads() {
|
|
49479
50014
|
const client = getClient();
|
|
49480
50015
|
const res = await client.query(`UPDATE todos_sync_records
|
|
@@ -49506,6 +50041,7 @@ var init_cloud = __esm(() => {
|
|
|
49506
50041
|
init_auth();
|
|
49507
50042
|
init_cloud_client();
|
|
49508
50043
|
init_postgres_adapter();
|
|
50044
|
+
init_postgres_sync();
|
|
49509
50045
|
init_comment_redaction_backfill();
|
|
49510
50046
|
});
|
|
49511
50047
|
|
|
@@ -49530,6 +50066,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49530
50066
|
schemas: {
|
|
49531
50067
|
Task: taskSchema,
|
|
49532
50068
|
Project: projectSchema,
|
|
50069
|
+
TaskList: taskListSchema,
|
|
49533
50070
|
TaskComment: taskCommentSchema,
|
|
49534
50071
|
CreateTaskInput: {
|
|
49535
50072
|
type: "object",
|
|
@@ -49558,12 +50095,65 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49558
50095
|
},
|
|
49559
50096
|
CreateProjectInput: {
|
|
49560
50097
|
type: "object",
|
|
50098
|
+
additionalProperties: false,
|
|
49561
50099
|
required: ["name", "path"],
|
|
49562
50100
|
properties: {
|
|
50101
|
+
name: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
50102
|
+
path: { type: "string", minLength: 1 },
|
|
50103
|
+
description: { type: "string" },
|
|
50104
|
+
task_list_id: { type: "string", minLength: 1, pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
|
50105
|
+
task_prefix: { type: "string", minLength: 1 }
|
|
50106
|
+
}
|
|
50107
|
+
},
|
|
50108
|
+
UpdateProjectInput: {
|
|
50109
|
+
type: "object",
|
|
50110
|
+
additionalProperties: false,
|
|
50111
|
+
minProperties: 1,
|
|
50112
|
+
properties: {
|
|
50113
|
+
name: { type: "string", minLength: 1 },
|
|
50114
|
+
path: { type: "string", minLength: 1 },
|
|
50115
|
+
description: { type: "string", nullable: true }
|
|
50116
|
+
}
|
|
50117
|
+
},
|
|
50118
|
+
RenameProjectInput: {
|
|
50119
|
+
type: "object",
|
|
50120
|
+
additionalProperties: false,
|
|
50121
|
+
required: ["new_slug"],
|
|
50122
|
+
properties: {
|
|
50123
|
+
new_slug: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
50124
|
+
name: { type: "string", minLength: 1 }
|
|
50125
|
+
}
|
|
50126
|
+
},
|
|
50127
|
+
ErrorResponse: {
|
|
50128
|
+
type: "object",
|
|
50129
|
+
required: ["error"],
|
|
50130
|
+
properties: {
|
|
50131
|
+
error: { type: "string" },
|
|
50132
|
+
code: { type: "string" },
|
|
50133
|
+
conflict: { type: "boolean" }
|
|
50134
|
+
}
|
|
50135
|
+
},
|
|
50136
|
+
CreateTaskListInput: {
|
|
50137
|
+
type: "object",
|
|
50138
|
+
additionalProperties: false,
|
|
50139
|
+
required: ["name"],
|
|
50140
|
+
properties: {
|
|
50141
|
+
name: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
50142
|
+
slug: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
50143
|
+
project_id: { type: "string" },
|
|
50144
|
+
description: { type: "string" },
|
|
50145
|
+
metadata: { type: "object", additionalProperties: true }
|
|
50146
|
+
}
|
|
50147
|
+
},
|
|
50148
|
+
UpdateTaskListInput: {
|
|
50149
|
+
type: "object",
|
|
50150
|
+
additionalProperties: false,
|
|
50151
|
+
minProperties: 1,
|
|
50152
|
+
properties: {
|
|
50153
|
+
slug: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
49563
50154
|
name: { type: "string" },
|
|
49564
|
-
path: { type: "string" },
|
|
49565
50155
|
description: { type: "string" },
|
|
49566
|
-
|
|
50156
|
+
metadata: { type: "object", additionalProperties: true }
|
|
49567
50157
|
}
|
|
49568
50158
|
},
|
|
49569
50159
|
CreateTaskCommentInput: {
|
|
@@ -49772,7 +50362,10 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49772
50362
|
required: true,
|
|
49773
50363
|
content: { "application/json": { schema: { $ref: "#/components/schemas/CreateProjectInput" } } }
|
|
49774
50364
|
},
|
|
49775
|
-
responses: {
|
|
50365
|
+
responses: {
|
|
50366
|
+
"201": { content: { "application/json": { schema: { type: "object", properties: { project: { $ref: "#/components/schemas/Project" } } } } } },
|
|
50367
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
50368
|
+
}
|
|
49776
50369
|
}
|
|
49777
50370
|
},
|
|
49778
50371
|
"/v1/projects/{id}": {
|
|
@@ -49781,6 +50374,86 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49781
50374
|
summary: "Get a project by id",
|
|
49782
50375
|
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
49783
50376
|
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { project: { $ref: "#/components/schemas/Project" } } } } } } }
|
|
50377
|
+
},
|
|
50378
|
+
patch: {
|
|
50379
|
+
operationId: "updateProject",
|
|
50380
|
+
summary: "Update a project",
|
|
50381
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50382
|
+
requestBody: {
|
|
50383
|
+
required: true,
|
|
50384
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateProjectInput" } } }
|
|
50385
|
+
},
|
|
50386
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { project: { $ref: "#/components/schemas/Project" } } } } } } }
|
|
50387
|
+
},
|
|
50388
|
+
delete: {
|
|
50389
|
+
operationId: "deleteProject",
|
|
50390
|
+
summary: "Delete a project",
|
|
50391
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50392
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
50393
|
+
}
|
|
50394
|
+
},
|
|
50395
|
+
"/v1/projects/{id}/rename": {
|
|
50396
|
+
post: {
|
|
50397
|
+
operationId: "renameProject",
|
|
50398
|
+
summary: "Atomically rename a project and its canonical task list",
|
|
50399
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50400
|
+
requestBody: {
|
|
50401
|
+
required: true,
|
|
50402
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/RenameProjectInput" } } }
|
|
50403
|
+
},
|
|
50404
|
+
responses: {
|
|
50405
|
+
"200": { content: { "application/json": { schema: { type: "object", properties: { project: { $ref: "#/components/schemas/Project" }, task_lists_updated: { type: "number" } } } } } },
|
|
50406
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
50407
|
+
}
|
|
50408
|
+
}
|
|
50409
|
+
},
|
|
50410
|
+
"/v1/task-lists": {
|
|
50411
|
+
get: {
|
|
50412
|
+
operationId: "listTaskLists",
|
|
50413
|
+
summary: "List task lists",
|
|
50414
|
+
parameters: [{ name: "project_id", in: "query", schema: { type: "string" } }],
|
|
50415
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { task_lists: { type: "array", items: { $ref: "#/components/schemas/TaskList" } }, count: { type: "number" } } } } } } }
|
|
50416
|
+
},
|
|
50417
|
+
post: {
|
|
50418
|
+
operationId: "createTaskList",
|
|
50419
|
+
summary: "Create a task list",
|
|
50420
|
+
requestBody: {
|
|
50421
|
+
required: true,
|
|
50422
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTaskListInput" } } }
|
|
50423
|
+
},
|
|
50424
|
+
responses: {
|
|
50425
|
+
"201": { content: { "application/json": { schema: { type: "object", properties: { task_list: { $ref: "#/components/schemas/TaskList" } } } } } },
|
|
50426
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
50427
|
+
}
|
|
50428
|
+
}
|
|
50429
|
+
},
|
|
50430
|
+
"/v1/task-lists/{id}": {
|
|
50431
|
+
get: {
|
|
50432
|
+
operationId: "getTaskList",
|
|
50433
|
+
summary: "Get a task list by id",
|
|
50434
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50435
|
+
responses: {
|
|
50436
|
+
"200": { content: { "application/json": { schema: { type: "object", properties: { task_list: { $ref: "#/components/schemas/TaskList" } } } } } }
|
|
50437
|
+
}
|
|
50438
|
+
},
|
|
50439
|
+
patch: {
|
|
50440
|
+
operationId: "updateTaskList",
|
|
50441
|
+
summary: "Update a task list",
|
|
50442
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50443
|
+
requestBody: {
|
|
50444
|
+
required: true,
|
|
50445
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateTaskListInput" } } }
|
|
50446
|
+
},
|
|
50447
|
+
responses: {
|
|
50448
|
+
"200": { content: { "application/json": { schema: { type: "object", properties: { task_list: { $ref: "#/components/schemas/TaskList" } } } } } },
|
|
50449
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
50450
|
+
}
|
|
50451
|
+
},
|
|
50452
|
+
delete: {
|
|
50453
|
+
operationId: "deleteTaskList",
|
|
50454
|
+
summary: "Delete a task list",
|
|
50455
|
+
parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
|
|
50456
|
+
responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
|
|
49784
50457
|
}
|
|
49785
50458
|
},
|
|
49786
50459
|
"/v1/stats": {
|
|
@@ -49847,7 +50520,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49847
50520
|
}
|
|
49848
50521
|
};
|
|
49849
50522
|
}
|
|
49850
|
-
var taskSchema, projectSchema, taskCommentSchema;
|
|
50523
|
+
var taskSchema, projectSchema, taskListSchema, taskCommentSchema;
|
|
49851
50524
|
var init_openapi = __esm(() => {
|
|
49852
50525
|
init_package_version();
|
|
49853
50526
|
taskSchema = {
|
|
@@ -49874,6 +50547,22 @@ var init_openapi = __esm(() => {
|
|
|
49874
50547
|
name: { type: "string" },
|
|
49875
50548
|
path: { type: "string" },
|
|
49876
50549
|
description: { type: "string", nullable: true },
|
|
50550
|
+
task_list_id: { type: "string", nullable: true },
|
|
50551
|
+
task_prefix: { type: "string", nullable: true },
|
|
50552
|
+
task_counter: { type: "number" },
|
|
50553
|
+
created_at: { type: "string" },
|
|
50554
|
+
updated_at: { type: "string" }
|
|
50555
|
+
}
|
|
50556
|
+
};
|
|
50557
|
+
taskListSchema = {
|
|
50558
|
+
type: "object",
|
|
50559
|
+
properties: {
|
|
50560
|
+
id: { type: "string" },
|
|
50561
|
+
project_id: { type: "string", nullable: true },
|
|
50562
|
+
slug: { type: "string" },
|
|
50563
|
+
name: { type: "string" },
|
|
50564
|
+
description: { type: "string", nullable: true },
|
|
50565
|
+
metadata: { type: "object", additionalProperties: true },
|
|
49877
50566
|
created_at: { type: "string" },
|
|
49878
50567
|
updated_at: { type: "string" }
|
|
49879
50568
|
}
|
|
@@ -49907,6 +50596,48 @@ function json2(body, status = 200) {
|
|
|
49907
50596
|
function error(status, message, extra) {
|
|
49908
50597
|
return json2({ error: message, ...extra ?? {} }, status);
|
|
49909
50598
|
}
|
|
50599
|
+
function validateProjectPatch(value) {
|
|
50600
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
50601
|
+
return { ok: false, message: "project patch must be an object" };
|
|
50602
|
+
const body = value;
|
|
50603
|
+
const allowed = new Set(["name", "path", "description"]);
|
|
50604
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
50605
|
+
if (unknown)
|
|
50606
|
+
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
50607
|
+
if (Object.keys(body).length === 0)
|
|
50608
|
+
return { ok: false, message: "project patch must not be empty" };
|
|
50609
|
+
if (body["name"] !== undefined && (typeof body["name"] !== "string" || !body["name"].trim()))
|
|
50610
|
+
return { ok: false, message: "name must be a non-empty string" };
|
|
50611
|
+
if (body["path"] !== undefined && (typeof body["path"] !== "string" || !body["path"].trim()))
|
|
50612
|
+
return { ok: false, message: "path must be a non-empty string" };
|
|
50613
|
+
if (body["description"] !== undefined && body["description"] !== null && typeof body["description"] !== "string")
|
|
50614
|
+
return { ok: false, message: "description must be a string or null" };
|
|
50615
|
+
return { ok: true, patch: body };
|
|
50616
|
+
}
|
|
50617
|
+
function validateProjectCreate(value) {
|
|
50618
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
50619
|
+
return { ok: false, message: "project body must be an object" };
|
|
50620
|
+
const body = value;
|
|
50621
|
+
const allowed = new Set(["name", "path", "description", "task_list_id", "task_prefix"]);
|
|
50622
|
+
const unknown = Object.keys(body).find((key) => !allowed.has(key));
|
|
50623
|
+
if (unknown)
|
|
50624
|
+
return { ok: false, message: `unknown project field: ${unknown}` };
|
|
50625
|
+
if (typeof body["name"] !== "string" || !body["name"].trim())
|
|
50626
|
+
return { ok: false, message: "name must be a non-empty string" };
|
|
50627
|
+
if (!normalizeSlug(body["name"]))
|
|
50628
|
+
return { ok: false, message: "name must produce a non-empty canonical slug" };
|
|
50629
|
+
if (typeof body["path"] !== "string" || !body["path"].trim())
|
|
50630
|
+
return { ok: false, message: "path must be a non-empty string" };
|
|
50631
|
+
if (body["description"] !== undefined && typeof body["description"] !== "string")
|
|
50632
|
+
return { ok: false, message: "description must be a string" };
|
|
50633
|
+
if (body["task_list_id"] !== undefined && !isCanonicalSlug(body["task_list_id"])) {
|
|
50634
|
+
return { ok: false, message: "task_list_id must be non-empty canonical kebab-case" };
|
|
50635
|
+
}
|
|
50636
|
+
if (body["task_prefix"] !== undefined && (typeof body["task_prefix"] !== "string" || !body["task_prefix"].trim())) {
|
|
50637
|
+
return { ok: false, message: "task_prefix must be a non-empty string" };
|
|
50638
|
+
}
|
|
50639
|
+
return { ok: true, input: body };
|
|
50640
|
+
}
|
|
49910
50641
|
async function readJson(req) {
|
|
49911
50642
|
try {
|
|
49912
50643
|
const text2 = await req.text();
|
|
@@ -50387,14 +51118,31 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50387
51118
|
}
|
|
50388
51119
|
if (method === "POST") {
|
|
50389
51120
|
const body = await readJson(req);
|
|
50390
|
-
if (!body
|
|
50391
|
-
return error(400, "
|
|
50392
|
-
|
|
50393
|
-
|
|
51121
|
+
if (!body)
|
|
51122
|
+
return error(400, "invalid JSON body");
|
|
51123
|
+
const validated = validateProjectCreate(body);
|
|
51124
|
+
if (!validated.ok)
|
|
51125
|
+
return error(400, validated.message);
|
|
51126
|
+
const project = await store.projects.create(validated.input, contextFromPrincipal(principal));
|
|
50394
51127
|
return json2({ project }, 201);
|
|
50395
51128
|
}
|
|
50396
51129
|
return error(405, `method ${method} not allowed on /v1/projects`);
|
|
50397
51130
|
}
|
|
51131
|
+
if (action === "rename") {
|
|
51132
|
+
if (method !== "POST")
|
|
51133
|
+
return error(405, `method ${method} not allowed on /v1/projects/:id/rename`);
|
|
51134
|
+
const body = await readJson(req);
|
|
51135
|
+
if (!body || typeof body.new_slug !== "string" || !body.new_slug.trim() || !normalizeSlug(body.new_slug)) {
|
|
51136
|
+
return error(400, "new_slug must be a non-empty string");
|
|
51137
|
+
}
|
|
51138
|
+
if (body.name !== undefined && (typeof body.name !== "string" || !body.name.trim())) {
|
|
51139
|
+
return error(400, "name must be a non-empty string");
|
|
51140
|
+
}
|
|
51141
|
+
const unknownField = Object.keys(body).find((key) => !["new_slug", "name"].includes(key));
|
|
51142
|
+
if (unknownField)
|
|
51143
|
+
return error(400, `unknown project rename field: ${unknownField}`);
|
|
51144
|
+
return json2(await store.projects.rename(id, body, contextFromPrincipal(principal)));
|
|
51145
|
+
}
|
|
50398
51146
|
if (method === "GET") {
|
|
50399
51147
|
const project = await store.projects.get(id);
|
|
50400
51148
|
return project ? json2({ project }) : error(404, "project not found");
|
|
@@ -50403,8 +51151,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50403
51151
|
const body = await readJson(req);
|
|
50404
51152
|
if (!body)
|
|
50405
51153
|
return error(400, "invalid JSON body");
|
|
50406
|
-
const
|
|
50407
|
-
|
|
51154
|
+
const validated = validateProjectPatch(body);
|
|
51155
|
+
if (!validated.ok)
|
|
51156
|
+
return error(400, validated.message);
|
|
51157
|
+
if (!await store.projects.get(id))
|
|
51158
|
+
return error(404, "project not found");
|
|
51159
|
+
const project = await store.projects.update(id, validated.patch);
|
|
51160
|
+
return json2({ project });
|
|
50408
51161
|
}
|
|
50409
51162
|
if (method === "DELETE") {
|
|
50410
51163
|
await store.projects.delete(id, contextFromPrincipal(principal));
|
|
@@ -50491,6 +51244,21 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50491
51244
|
const body = await readJson(req);
|
|
50492
51245
|
if (!body || typeof body.name !== "string" || !body.name.trim())
|
|
50493
51246
|
return error(400, "name is required");
|
|
51247
|
+
const unknownField = Object.keys(body).find((key) => !["name", "slug", "project_id", "description", "metadata"].includes(key));
|
|
51248
|
+
if (unknownField)
|
|
51249
|
+
return error(400, `unsupported task-list create field: ${unknownField}`);
|
|
51250
|
+
if (body.slug !== undefined && typeof body.slug !== "string")
|
|
51251
|
+
return error(400, "slug must be a string");
|
|
51252
|
+
if (body.project_id !== undefined && (typeof body.project_id !== "string" || !body.project_id.trim()))
|
|
51253
|
+
return error(400, "project_id must be a non-empty string");
|
|
51254
|
+
if (body.description !== undefined && typeof body.description !== "string")
|
|
51255
|
+
return error(400, "description must be a string");
|
|
51256
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
51257
|
+
return error(400, "metadata must be an object");
|
|
51258
|
+
}
|
|
51259
|
+
if (!normalizeSlug(body.slug === undefined ? body.name : body.slug)) {
|
|
51260
|
+
return error(400, "task-list slug must be non-empty kebab-case");
|
|
51261
|
+
}
|
|
50494
51262
|
const taskList = await store.taskLists.create(body, contextFromPrincipal(principal));
|
|
50495
51263
|
return json2({ task_list: taskList }, 201);
|
|
50496
51264
|
}
|
|
@@ -50498,6 +51266,29 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50498
51266
|
const taskList = await store.taskLists.get(id);
|
|
50499
51267
|
return taskList ? json2({ task_list: taskList }) : error(404, "task list not found");
|
|
50500
51268
|
}
|
|
51269
|
+
if (id && (method === "PATCH" || method === "PUT")) {
|
|
51270
|
+
const body = await readJson(req);
|
|
51271
|
+
if (!body)
|
|
51272
|
+
return error(400, "invalid JSON body");
|
|
51273
|
+
const unknownField = Object.keys(body).find((key) => !["slug", "name", "description", "metadata"].includes(key));
|
|
51274
|
+
if (unknownField)
|
|
51275
|
+
return error(400, `unsupported task-list update field: ${unknownField}`);
|
|
51276
|
+
if (Object.keys(body).length === 0)
|
|
51277
|
+
return error(400, "task-list update must not be empty");
|
|
51278
|
+
if (body.slug !== undefined && (typeof body.slug !== "string" || !normalizeSlug(body.slug)))
|
|
51279
|
+
return error(400, "slug must be a non-empty string");
|
|
51280
|
+
if (body.name !== undefined && (typeof body.name !== "string" || !body.name.trim()))
|
|
51281
|
+
return error(400, "name must be a non-empty string");
|
|
51282
|
+
if (body.description !== undefined && typeof body.description !== "string")
|
|
51283
|
+
return error(400, "description must be a string");
|
|
51284
|
+
if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata))) {
|
|
51285
|
+
return error(400, "metadata must be an object");
|
|
51286
|
+
}
|
|
51287
|
+
if (!await store.taskLists.get(id))
|
|
51288
|
+
return error(404, "task list not found");
|
|
51289
|
+
const taskList = await store.taskLists.update(id, body);
|
|
51290
|
+
return json2({ task_list: taskList });
|
|
51291
|
+
}
|
|
50501
51292
|
if (id && method === "DELETE") {
|
|
50502
51293
|
const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
|
|
50503
51294
|
return deleted ? json2({ deleted: true, id }) : error(404, "task list not found");
|
|
@@ -50570,6 +51361,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50570
51361
|
} catch (e) {
|
|
50571
51362
|
if (e instanceof LockError)
|
|
50572
51363
|
return error(409, e.message, { code: LockError.code });
|
|
51364
|
+
if (e instanceof ResourceConflictError)
|
|
51365
|
+
return error(409, e.message, { code: e.code, conflict: true });
|
|
51366
|
+
if (e instanceof ProjectNotFoundError)
|
|
51367
|
+
return error(404, e.message, { code: ProjectNotFoundError.code });
|
|
50573
51368
|
return error(500, e.message || "internal error");
|
|
50574
51369
|
}
|
|
50575
51370
|
}
|