@plandesk/api 1.0.0-beta.8 → 1.0.2
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/agent-keys.d.ts +26 -2
- package/dist/agent-keys.js +22 -2
- package/dist/auth-context.d.ts +17 -1
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +25 -6
- package/dist/better-auth.js +4 -3
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +190 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/invitations.d.ts +13 -2
- package/dist/invitations.js +14 -2
- package/dist/organizations.d.ts +3 -0
- package/dist/organizations.js +18 -0
- package/dist/projection.d.ts +28 -0
- package/dist/projection.js +29 -1
- package/dist/routes/auth.js +21 -5
- package/dist/routes/comments.js +3 -1
- package/dist/routes/orgs.js +139 -24
- package/dist/routes/projects.js +36 -5
- package/dist/routes/shares.js +32 -0
- package/dist/routes/tasks.js +3 -0
- package/dist/serialize.d.ts +2 -0
- package/dist/serialize.js +1 -0
- package/dist/server.js +9 -6
- package/dist/services/agent-runs.js +18 -0
- package/dist/services/artifacts.js +18 -0
- package/dist/services/canvas.js +9 -0
- package/dist/services/comments.js +63 -13
- package/dist/services/documents.js +36 -0
- package/dist/services/files.js +19 -1
- package/dist/services/folders.js +27 -0
- package/dist/services/goals.js +45 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1 -1
- package/dist/services/notes.js +27 -0
- package/dist/services/projects.d.ts +27 -0
- package/dist/services/projects.js +113 -4
- package/dist/services/scope.d.ts +4 -0
- package/dist/services/scope.js +22 -0
- package/dist/services/share.d.ts +25 -3
- package/dist/services/share.js +160 -9
- package/dist/services/sync.d.ts +1 -1
- package/dist/services/sync.js +52 -1
- package/dist/services/tags.js +18 -0
- package/dist/services/tasks.d.ts +1 -1
- package/dist/services/tasks.js +22 -0
- package/dist/test-helpers.d.ts +1 -0
- package/dist/test-helpers.js +14 -0
- package/dist/vercel.js +9 -1
- package/dist/worker.js +1 -1
- package/package.json +3 -3
- package/web/assets/index-BBNvafSX.js +358 -0
- package/web/assets/index-BqoMRSUH.css +2 -0
- package/web/index.html +2 -2
- package/web/assets/index-aQ0kLFAX.js +0 -358
- package/web/assets/index-nXT4L9C-.css +0 -2
package/dist/server.js
CHANGED
|
@@ -19,16 +19,13 @@ import { createGoalsRouter } from './routes/goals.js';
|
|
|
19
19
|
import { createSubmissionsRouter } from './routes/submissions.js';
|
|
20
20
|
import { createOrgsRouter } from './routes/orgs.js';
|
|
21
21
|
import { createServices } from './services/index.js';
|
|
22
|
-
import { ProjectNotInOrgError } from './services/scope.js';
|
|
22
|
+
import { ProjectNotInOrgError, WorkspaceNotFoundError } from './services/scope.js';
|
|
23
23
|
import { ReadOnlyTokenError } from './auth-context.js';
|
|
24
24
|
import { PermissionDeniedError } from './permissions.js';
|
|
25
25
|
export function createApp(deps) {
|
|
26
|
-
const services = deps.services ?? createServices({ db: deps.db });
|
|
27
|
-
const { projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, syncService, fileService, artifactService, shareService, } = services;
|
|
28
26
|
const bindHost = deps.bindHost ?? '127.0.0.1';
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
// by AuthContext and mounted at /api/auth/* (BA4a session recognition).
|
|
27
|
+
// Create better-auth before services so workspace resolution is available
|
|
28
|
+
// to the project service (REQ-6).
|
|
32
29
|
const betterAuthInstance = deps.betterAuthInstance ??
|
|
33
30
|
(deps.betterAuth !== undefined
|
|
34
31
|
? createBetterAuth({
|
|
@@ -39,6 +36,9 @@ export function createApp(deps) {
|
|
|
39
36
|
github: deps.github,
|
|
40
37
|
})
|
|
41
38
|
: undefined);
|
|
39
|
+
const services = deps.services ?? createServices({ db: deps.db, auth: betterAuthInstance });
|
|
40
|
+
const { projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, syncService, fileService, artifactService, shareService, } = services;
|
|
41
|
+
const app = new Hono();
|
|
42
42
|
// Always-on org resolution (better-auth apiKey/session, loopback, or guest).
|
|
43
43
|
app.use('*', createOrgAuthMiddleware({
|
|
44
44
|
db: deps.db,
|
|
@@ -53,6 +53,9 @@ export function createApp(deps) {
|
|
|
53
53
|
if (err instanceof ProjectNotInOrgError) {
|
|
54
54
|
return c.json({ error: 'not_found' }, 404);
|
|
55
55
|
}
|
|
56
|
+
if (err instanceof WorkspaceNotFoundError) {
|
|
57
|
+
return c.json({ error: 'not_found' }, 404);
|
|
58
|
+
}
|
|
56
59
|
if (err instanceof ReadOnlyTokenError || err instanceof PermissionDeniedError) {
|
|
57
60
|
return c.json({ error: 'forbidden' }, 403);
|
|
58
61
|
}
|
|
@@ -62,6 +62,15 @@ export function createAgentRunService(deps) {
|
|
|
62
62
|
if (!run) {
|
|
63
63
|
return undefined;
|
|
64
64
|
}
|
|
65
|
+
try {
|
|
66
|
+
await assertProjectInOrg(db, run.projectId, resolveOrgId(deps));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
65
74
|
if (terminalStatuses.has(run.status)) {
|
|
66
75
|
throw new InvalidAgentRunError('Agent run is already complete');
|
|
67
76
|
}
|
|
@@ -74,6 +83,15 @@ export function createAgentRunService(deps) {
|
|
|
74
83
|
if (!run) {
|
|
75
84
|
return undefined;
|
|
76
85
|
}
|
|
86
|
+
try {
|
|
87
|
+
await assertProjectInOrg(db, run.projectId, resolveOrgId(deps));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
77
95
|
if (terminalStatuses.has(run.status)) {
|
|
78
96
|
throw new InvalidAgentRunError('Agent run is already complete');
|
|
79
97
|
}
|
|
@@ -53,6 +53,15 @@ export function createArtifactService(deps) {
|
|
|
53
53
|
if (!artifact) {
|
|
54
54
|
return undefined;
|
|
55
55
|
}
|
|
56
|
+
try {
|
|
57
|
+
await assertProjectInOrg(db, artifact.projectId, resolveOrgId(deps));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
56
65
|
return serializeArtifact(artifact);
|
|
57
66
|
},
|
|
58
67
|
async update(id, input) {
|
|
@@ -61,6 +70,15 @@ export function createArtifactService(deps) {
|
|
|
61
70
|
if (!existing) {
|
|
62
71
|
return undefined;
|
|
63
72
|
}
|
|
73
|
+
try {
|
|
74
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
64
82
|
if (input.title !== undefined) {
|
|
65
83
|
assertNonEmptyTitle(input.title);
|
|
66
84
|
}
|
package/dist/services/canvas.js
CHANGED
|
@@ -167,6 +167,15 @@ export function createCanvasService(deps) {
|
|
|
167
167
|
},
|
|
168
168
|
async deleteEdge(projectId, edgeId) {
|
|
169
169
|
assertPermission(deps, 'edge', 'delete');
|
|
170
|
+
try {
|
|
171
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
170
179
|
const edge = await getEdgeByProjectAndId(db, projectId, edgeId);
|
|
171
180
|
if (!edge) {
|
|
172
181
|
return false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createComment, deleteComment as dbDeleteComment, getComment as dbGetComment, getDocument as dbGetDocument, getNote as dbGetNote,
|
|
1
|
+
import { createComment, deleteComment as dbDeleteComment, getComment as dbGetComment, getDocument as dbGetDocument, getNote as dbGetNote, getSubmission, getTask, listCommentsByProject as dbListCommentsByProject, listCommentsByTarget as dbListCommentsByTarget, listCommentsByTargetInProject as dbListCommentsByTargetInProject, updateComment as dbUpdateComment, } from '@plandesk/db';
|
|
2
2
|
import { serializeComment } from '../serialize.js';
|
|
3
3
|
import { assertPermission, resolveOrgId } from './org-scope.js';
|
|
4
4
|
import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
|
|
@@ -39,6 +39,15 @@ export function createCommentService(deps) {
|
|
|
39
39
|
if (!projectId) {
|
|
40
40
|
return undefined;
|
|
41
41
|
}
|
|
42
|
+
try {
|
|
43
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
42
51
|
assertNonEmptyBody(input.body);
|
|
43
52
|
const comment = await createComment(db, {
|
|
44
53
|
projectId,
|
|
@@ -55,8 +64,14 @@ export function createCommentService(deps) {
|
|
|
55
64
|
// in the file identity mean it travels in the body/query, never a path seg.
|
|
56
65
|
async createForArtifact(projectId, artifactId, input) {
|
|
57
66
|
assertPermission(deps, 'comment', 'create');
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
try {
|
|
68
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
60
75
|
}
|
|
61
76
|
if (artifactId.trim() === '') {
|
|
62
77
|
throw new InvalidCommentError('Artifact id must not be empty');
|
|
@@ -73,16 +88,33 @@ export function createCommentService(deps) {
|
|
|
73
88
|
return serializeComment(comment);
|
|
74
89
|
},
|
|
75
90
|
async listForArtifact(projectId, artifactId, options) {
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
try {
|
|
92
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
78
99
|
}
|
|
79
|
-
|
|
100
|
+
// Artifact ids (file paths) are not globally unique — scope by projectId
|
|
101
|
+
// so project A's list never returns project B's same-path comment.
|
|
102
|
+
return (await dbListCommentsByTargetInProject(db, 'artifact', artifactId, projectId, options)).map(serializeComment);
|
|
80
103
|
},
|
|
81
104
|
async listByTarget(target, options) {
|
|
82
105
|
const projectId = await targetProjectId(db, target);
|
|
83
106
|
if (!projectId) {
|
|
84
107
|
return undefined;
|
|
85
108
|
}
|
|
109
|
+
try {
|
|
110
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
86
118
|
return (await dbListCommentsByTarget(db, target.type, target.id, options)).map(serializeComment);
|
|
87
119
|
},
|
|
88
120
|
async resolveTargetProjectId(target) {
|
|
@@ -109,6 +141,22 @@ export function createCommentService(deps) {
|
|
|
109
141
|
if (!existing) {
|
|
110
142
|
return undefined;
|
|
111
143
|
}
|
|
144
|
+
const projectId = await targetProjectId(db, {
|
|
145
|
+
type: existing.targetType,
|
|
146
|
+
id: existing.targetId,
|
|
147
|
+
});
|
|
148
|
+
if (!projectId) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
112
160
|
if (input.body !== undefined) {
|
|
113
161
|
assertNonEmptyBody(input.body);
|
|
114
162
|
}
|
|
@@ -116,13 +164,6 @@ export function createCommentService(deps) {
|
|
|
116
164
|
if (!comment) {
|
|
117
165
|
return undefined;
|
|
118
166
|
}
|
|
119
|
-
const projectId = await targetProjectId(db, {
|
|
120
|
-
type: comment.targetType,
|
|
121
|
-
id: comment.targetId,
|
|
122
|
-
});
|
|
123
|
-
if (!projectId) {
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
167
|
return serializeComment(comment);
|
|
127
168
|
},
|
|
128
169
|
async delete(id) {
|
|
@@ -138,6 +179,15 @@ export function createCommentService(deps) {
|
|
|
138
179
|
if (!projectId) {
|
|
139
180
|
return false;
|
|
140
181
|
}
|
|
182
|
+
try {
|
|
183
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
141
191
|
const deleted = await dbDeleteComment(db, id);
|
|
142
192
|
if (!deleted) {
|
|
143
193
|
return false;
|
|
@@ -103,6 +103,15 @@ export function createDocumentService(deps) {
|
|
|
103
103
|
if (!document) {
|
|
104
104
|
return undefined;
|
|
105
105
|
}
|
|
106
|
+
try {
|
|
107
|
+
await assertProjectInOrg(db, document.projectId, resolveOrgId(deps));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
106
115
|
return serializeDocument(document);
|
|
107
116
|
},
|
|
108
117
|
async update(id, input) {
|
|
@@ -111,6 +120,15 @@ export function createDocumentService(deps) {
|
|
|
111
120
|
if (!existing) {
|
|
112
121
|
return undefined;
|
|
113
122
|
}
|
|
123
|
+
try {
|
|
124
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
114
132
|
if (input.linkedTaskId !== undefined && input.linkedTaskId !== null) {
|
|
115
133
|
await assertTaskInProject(db, existing.projectId, input.linkedTaskId);
|
|
116
134
|
}
|
|
@@ -138,6 +156,15 @@ export function createDocumentService(deps) {
|
|
|
138
156
|
if (!document) {
|
|
139
157
|
return undefined;
|
|
140
158
|
}
|
|
159
|
+
try {
|
|
160
|
+
await assertProjectInOrg(db, document.projectId, resolveOrgId(deps));
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
141
168
|
return serializeDocument(document);
|
|
142
169
|
},
|
|
143
170
|
async delete(id) {
|
|
@@ -146,6 +173,15 @@ export function createDocumentService(deps) {
|
|
|
146
173
|
if (!existing) {
|
|
147
174
|
return false;
|
|
148
175
|
}
|
|
176
|
+
try {
|
|
177
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
149
185
|
await withTransaction(db, async (tx) => {
|
|
150
186
|
await detachDocumentChildren(tx, id);
|
|
151
187
|
await deleteCommentsByTarget(tx, 'document', id);
|
package/dist/services/files.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { getFile } from '@plandesk/db';
|
|
1
|
+
import { getFile, getFileInOrg } from '@plandesk/db';
|
|
2
|
+
import { tryGetAuthContext } from '../auth-context.js';
|
|
2
3
|
import { assertPermission, resolveOrgId } from './org-scope.js';
|
|
3
4
|
import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
|
|
4
5
|
export function createFileService(deps) {
|
|
@@ -31,6 +32,23 @@ export function createFileService(deps) {
|
|
|
31
32
|
return { id, url, filename: file.filename, mime: file.mime, size: file.size };
|
|
32
33
|
},
|
|
33
34
|
async get(id) {
|
|
35
|
+
const auth = tryGetAuthContext();
|
|
36
|
+
if (auth === undefined || auth.kind === 'guest') {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const file = await getFileInOrg(db, id, auth.orgId);
|
|
40
|
+
if (!file) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
await assertProjectInOrg(db, file.projectId, resolveOrgId(deps));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
34
52
|
const resolved = await storage.resolve(id);
|
|
35
53
|
return resolved ?? undefined;
|
|
36
54
|
},
|
package/dist/services/folders.js
CHANGED
|
@@ -75,6 +75,15 @@ export function createFolderService(deps) {
|
|
|
75
75
|
if (!folder) {
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
|
+
try {
|
|
79
|
+
await assertProjectInOrg(db, folder.projectId, resolveOrgId(deps));
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
78
87
|
return serializeFolder(folder);
|
|
79
88
|
},
|
|
80
89
|
async update(id, input) {
|
|
@@ -83,6 +92,15 @@ export function createFolderService(deps) {
|
|
|
83
92
|
if (!existing) {
|
|
84
93
|
return undefined;
|
|
85
94
|
}
|
|
95
|
+
try {
|
|
96
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
86
104
|
if (input.name !== undefined) {
|
|
87
105
|
assertNonEmptyName(input.name);
|
|
88
106
|
}
|
|
@@ -105,6 +123,15 @@ export function createFolderService(deps) {
|
|
|
105
123
|
if (!existing) {
|
|
106
124
|
return false;
|
|
107
125
|
}
|
|
126
|
+
try {
|
|
127
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
108
135
|
// Never orphan: children folders and contained documents move to the
|
|
109
136
|
// deleted folder's parent (or root when the folder was at root).
|
|
110
137
|
await withTransaction(db, async (tx) => {
|
package/dist/services/goals.js
CHANGED
|
@@ -190,6 +190,15 @@ export function createGoalService(deps) {
|
|
|
190
190
|
if (!goal) {
|
|
191
191
|
return undefined;
|
|
192
192
|
}
|
|
193
|
+
try {
|
|
194
|
+
await assertProjectInOrg(db, goal.projectId, resolveOrgId(deps));
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
193
202
|
return {
|
|
194
203
|
...serializeGoal(goal),
|
|
195
204
|
cycle_tasks: await cycleTasksForGoal(db, goal.projectId, goalId),
|
|
@@ -213,6 +222,15 @@ export function createGoalService(deps) {
|
|
|
213
222
|
if (!existing) {
|
|
214
223
|
return undefined;
|
|
215
224
|
}
|
|
225
|
+
try {
|
|
226
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
216
234
|
validateVerificationSurfaceInput(input.verificationSurface);
|
|
217
235
|
const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, input));
|
|
218
236
|
if (!goal) {
|
|
@@ -226,6 +244,15 @@ export function createGoalService(deps) {
|
|
|
226
244
|
if (!existing) {
|
|
227
245
|
return undefined;
|
|
228
246
|
}
|
|
247
|
+
try {
|
|
248
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
229
256
|
if (existing.status !== 'active') {
|
|
230
257
|
throw new InvalidGoalTransitionError('Goal can only be paused from active status');
|
|
231
258
|
}
|
|
@@ -241,6 +268,15 @@ export function createGoalService(deps) {
|
|
|
241
268
|
if (!existing) {
|
|
242
269
|
return undefined;
|
|
243
270
|
}
|
|
271
|
+
try {
|
|
272
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
244
280
|
if (existing.status !== 'paused') {
|
|
245
281
|
throw new InvalidGoalTransitionError('Goal can only be resumed from paused status');
|
|
246
282
|
}
|
|
@@ -256,6 +292,15 @@ export function createGoalService(deps) {
|
|
|
256
292
|
if (!existing) {
|
|
257
293
|
return undefined;
|
|
258
294
|
}
|
|
295
|
+
try {
|
|
296
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
259
304
|
if (existing.status === 'complete') {
|
|
260
305
|
throw new InvalidGoalTransitionError('Goal is already complete');
|
|
261
306
|
}
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Db } from '@plandesk/db';
|
|
2
|
+
import type { BetterAuthInstance } from '../better-auth.js';
|
|
2
3
|
import { type StorageAdapter } from '../storage/index.js';
|
|
3
4
|
import { type CanvasService } from './canvas.js';
|
|
4
5
|
import { type CommentService } from './comments.js';
|
|
@@ -19,6 +20,8 @@ export type ServicesDeps = {
|
|
|
19
20
|
storage?: StorageAdapter;
|
|
20
21
|
/** Fixed org scope for unit tests; production request path uses auth context. */
|
|
21
22
|
orgId?: string;
|
|
23
|
+
/** better-auth instance for workspace resolution (project creation). */
|
|
24
|
+
auth?: BetterAuthInstance;
|
|
22
25
|
};
|
|
23
26
|
export type Services = {
|
|
24
27
|
projectService: ProjectService;
|
package/dist/services/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import { createTaskService } from './tasks.js';
|
|
|
14
14
|
import { createShareService } from './share.js';
|
|
15
15
|
import { createSyncService } from './sync.js';
|
|
16
16
|
export function createServices(deps) {
|
|
17
|
-
const scoped = { db: deps.db, orgId: deps.orgId };
|
|
17
|
+
const scoped = { db: deps.db, orgId: deps.orgId, auth: deps.auth };
|
|
18
18
|
const storage = deps.storage ?? createStorageAdapter({ db: deps.db });
|
|
19
19
|
const projectService = createProjectService(scoped);
|
|
20
20
|
const goalService = createGoalService(scoped);
|
package/dist/services/notes.js
CHANGED
|
@@ -52,6 +52,15 @@ export function createNoteService(deps) {
|
|
|
52
52
|
if (!note) {
|
|
53
53
|
return undefined;
|
|
54
54
|
}
|
|
55
|
+
try {
|
|
56
|
+
await assertProjectInOrg(db, note.projectId, resolveOrgId(deps));
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
55
64
|
return serializeNote(note);
|
|
56
65
|
},
|
|
57
66
|
async update(id, input) {
|
|
@@ -60,6 +69,15 @@ export function createNoteService(deps) {
|
|
|
60
69
|
if (!existing) {
|
|
61
70
|
return undefined;
|
|
62
71
|
}
|
|
72
|
+
try {
|
|
73
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
63
81
|
if (input.title !== undefined) {
|
|
64
82
|
assertNonEmptyTitle(input.title);
|
|
65
83
|
}
|
|
@@ -75,6 +93,15 @@ export function createNoteService(deps) {
|
|
|
75
93
|
if (!existing) {
|
|
76
94
|
return false;
|
|
77
95
|
}
|
|
96
|
+
try {
|
|
97
|
+
await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
78
105
|
const deleted = await dbDeleteNote(db, id);
|
|
79
106
|
if (!deleted) {
|
|
80
107
|
return false;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Db, type TaskStatus } from '@plandesk/db';
|
|
2
|
+
import type { BetterAuthInstance } from '../better-auth.js';
|
|
2
3
|
import { serializeEdge, serializeProject, serializeTask, type PaginationParams, type SerializedDocument, type TaskStatusSummary } from '../serialize.js';
|
|
3
4
|
import { type OrgScopedDeps } from './org-scope.js';
|
|
4
5
|
type SerializedProject = ReturnType<typeof serializeProject>;
|
|
@@ -33,6 +34,10 @@ export type ScaffoldPlanInput = {
|
|
|
33
34
|
/** Name for a new project. Required when projectId is omitted; ignored when it is set. */
|
|
34
35
|
name?: string;
|
|
35
36
|
description?: string | null;
|
|
37
|
+
/** Target workspace (team) for a new project. A workspace/project-scoped agent
|
|
38
|
+
* key must target its own scope (or omit this to default to it); a mismatch is
|
|
39
|
+
* a 404. Ignored when projectId is set. */
|
|
40
|
+
workspaceId?: string;
|
|
36
41
|
tasks: ScaffoldTaskInput[];
|
|
37
42
|
edges?: ScaffoldEdgeInput[];
|
|
38
43
|
documents?: ScaffoldDocumentInput[];
|
|
@@ -51,10 +56,12 @@ export type ScaffoldPlanResult = {
|
|
|
51
56
|
};
|
|
52
57
|
export type ProjectServiceDeps = OrgScopedDeps & {
|
|
53
58
|
db: Db;
|
|
59
|
+
auth?: BetterAuthInstance;
|
|
54
60
|
};
|
|
55
61
|
export type CreateProjectInput = {
|
|
56
62
|
name: string;
|
|
57
63
|
description?: string | null;
|
|
64
|
+
workspaceId?: string;
|
|
58
65
|
};
|
|
59
66
|
export type UpdateProjectInput = {
|
|
60
67
|
name?: string;
|
|
@@ -65,6 +72,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
|
|
|
65
72
|
id: string;
|
|
66
73
|
name: string;
|
|
67
74
|
description: string | null;
|
|
75
|
+
workspace_id: string;
|
|
68
76
|
created_at: string;
|
|
69
77
|
updated_at: string;
|
|
70
78
|
}>;
|
|
@@ -72,6 +80,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
|
|
|
72
80
|
id: string;
|
|
73
81
|
name: string;
|
|
74
82
|
description: string | null;
|
|
83
|
+
workspace_id: string;
|
|
75
84
|
created_at: string;
|
|
76
85
|
updated_at: string;
|
|
77
86
|
}[]>;
|
|
@@ -80,6 +89,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
|
|
|
80
89
|
id: string;
|
|
81
90
|
name: string;
|
|
82
91
|
description: string | null;
|
|
92
|
+
workspace_id: string;
|
|
83
93
|
created_at: string;
|
|
84
94
|
updated_at: string;
|
|
85
95
|
} | undefined>;
|
|
@@ -87,6 +97,23 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
|
|
|
87
97
|
id: string;
|
|
88
98
|
name: string;
|
|
89
99
|
description: string | null;
|
|
100
|
+
workspace_id: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
updated_at: string;
|
|
103
|
+
} | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* Move a project to another workspace (team) in the caller's org.
|
|
106
|
+
*
|
|
107
|
+
* Owner-gated: a workspace/project-scoped agent key must not move projects
|
|
108
|
+
* (it would drag a project out of its scope), so scoped callers are rejected
|
|
109
|
+
* before any existence check. Requires project:create authority (owner/admin).
|
|
110
|
+
* The target team must exist in the caller's org; otherwise 404.
|
|
111
|
+
*/
|
|
112
|
+
moveProjectToWorkspace(id: string, workspaceId: string): Promise<{
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
description: string | null;
|
|
116
|
+
workspace_id: string;
|
|
90
117
|
created_at: string;
|
|
91
118
|
updated_at: string;
|
|
92
119
|
} | undefined>;
|