@plandesk/api 0.20.0 → 1.0.0-beta.1
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/access-control.d.ts +150 -0
- package/dist/access-control.js +63 -0
- package/dist/agent-keys.d.ts +81 -0
- package/dist/agent-keys.js +181 -0
- package/dist/auth-context.d.ts +60 -0
- package/dist/auth-context.js +41 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +357 -0
- package/dist/better-auth.d.ts +66 -0
- package/dist/better-auth.js +106 -0
- package/dist/github.d.ts +57 -0
- package/dist/github.js +115 -0
- package/dist/hosted-auth.d.ts +23 -0
- package/dist/hosted-auth.js +49 -0
- package/dist/identity.d.ts +38 -0
- package/dist/identity.js +163 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +12 -2
- package/dist/invitations.d.ts +94 -0
- package/dist/invitations.js +211 -0
- package/dist/organizations.d.ts +32 -0
- package/dist/organizations.js +63 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/permissions.js +42 -0
- package/dist/projection.d.ts +1 -1
- package/dist/projection.js +5 -5
- package/dist/routes/agent-runs.js +3 -3
- package/dist/routes/artifacts.js +6 -6
- package/dist/routes/auth.d.ts +13 -0
- package/dist/routes/auth.js +83 -0
- package/dist/routes/canvas.js +5 -5
- package/dist/routes/comments.js +11 -11
- package/dist/routes/documents.js +10 -10
- package/dist/routes/folders.js +8 -8
- package/dist/routes/goals.js +11 -11
- package/dist/routes/notes.js +8 -8
- package/dist/routes/orgs.d.ts +17 -0
- package/dist/routes/orgs.js +241 -0
- package/dist/routes/projects.js +13 -13
- package/dist/routes/shares.js +92 -3
- package/dist/routes/submissions.js +7 -8
- package/dist/routes/tags.js +6 -6
- package/dist/routes/tasks.js +14 -3
- package/dist/routes/tokens.js +4 -4
- package/dist/serialize.d.ts +3 -3
- package/dist/server.d.ts +20 -2
- package/dist/server.js +57 -9
- package/dist/services/agent-runs.d.ts +13 -14
- package/dist/services/agent-runs.js +39 -41
- package/dist/services/artifacts.d.ts +6 -7
- package/dist/services/artifacts.js +30 -26
- package/dist/services/canvas.d.ts +11 -12
- package/dist/services/canvas.js +58 -41
- package/dist/services/comments.d.ts +11 -12
- package/dist/services/comments.js +44 -64
- package/dist/services/documents.d.ts +10 -11
- package/dist/services/documents.js +74 -55
- package/dist/services/files.d.ts +2 -1
- package/dist/services/files.js +13 -5
- package/dist/services/folders.d.ts +7 -8
- package/dist/services/folders.js +45 -33
- package/dist/services/goals.d.ts +24 -25
- package/dist/services/goals.js +60 -53
- package/dist/services/index.d.ts +2 -5
- package/dist/services/index.js +15 -20
- package/dist/services/notes.d.ts +7 -8
- package/dist/services/notes.js +35 -23
- package/dist/services/org-scope.d.ts +22 -0
- package/dist/services/org-scope.js +38 -0
- package/dist/services/projects.d.ts +12 -13
- package/dist/services/projects.js +90 -58
- package/dist/services/scope.d.ts +12 -0
- package/dist/services/scope.js +29 -0
- package/dist/services/share.d.ts +74 -10
- package/dist/services/share.js +213 -23
- package/dist/services/sync.d.ts +7 -29
- package/dist/services/sync.js +37 -142
- package/dist/services/tags.d.ts +6 -7
- package/dist/services/tags.js +34 -22
- package/dist/services/tasks.d.ts +24 -17
- package/dist/services/tasks.js +97 -58
- package/dist/services/tokens.d.ts +6 -5
- package/dist/services/tokens.js +13 -6
- package/dist/session.d.ts +5 -0
- package/dist/session.js +8 -0
- package/dist/storage/local.js +16 -10
- package/dist/storage/s3.js +11 -6
- package/dist/test-ba-org.d.ts +16 -0
- package/dist/test-ba-org.js +35 -0
- package/dist/test-helpers.d.ts +21 -5
- package/dist/test-helpers.js +45 -8
- package/dist/vercel.d.ts +3 -0
- package/dist/vercel.js +62 -0
- package/dist/worker.d.ts +44 -0
- package/dist/worker.js +101 -0
- package/package.json +8 -5
- package/web/assets/index-D29SyYFS.css +2 -0
- package/web/assets/index-tBJ_K3ur.js +358 -0
- package/web/index.html +2 -2
- package/web/assets/index-CeBvZ-aq.css +0 -2
- package/web/assets/index-CuRAxImC.js +0 -358
package/dist/services/goals.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { createGoal, createTask, getGoal, getProject, InvalidGoalStatusError, isGoalStatus, listGoals, listTagsByTaskForProject, listTasks, updateGoal, updateGoalStatus, updateTask, } from '@plandesk/db';
|
|
1
|
+
import { withTransaction, createGoal, createTask, getGoal, getProject, InvalidGoalStatusError, isGoalStatus, listGoals, listTagsByTaskForProject, listTasks, updateGoal, updateGoalStatus, updateTask, } from '@plandesk/db';
|
|
2
2
|
import { serializeGoal, serializeTask } from '../serialize.js';
|
|
3
|
+
import { assertPermission, resolveOrgId } from './org-scope.js';
|
|
4
|
+
import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
|
|
3
5
|
export class InvalidGoalTransitionError extends Error {
|
|
4
6
|
constructor(message) {
|
|
5
7
|
super(message);
|
|
@@ -112,17 +114,17 @@ function validateVerificationSurfaceInput(raw) {
|
|
|
112
114
|
function acceptanceBlockMarker(goalId) {
|
|
113
115
|
return `Acceptance-block-for: ${goalId}`;
|
|
114
116
|
}
|
|
115
|
-
function hasUnresolvedBlockingTask(db, projectId, goalId) {
|
|
117
|
+
async function hasUnresolvedBlockingTask(db, projectId, goalId) {
|
|
116
118
|
const marker = acceptanceBlockMarker(goalId);
|
|
117
|
-
return listTasks(db, projectId).some((task) => task.goalId === goalId && task.status === 'scope' && task.description?.includes(marker));
|
|
119
|
+
return (await listTasks(db, projectId)).some((task) => task.goalId === goalId && task.status === 'scope' && task.description?.includes(marker));
|
|
118
120
|
}
|
|
119
121
|
// When acceptance finally passes, the remediation task filed on the earlier red
|
|
120
122
|
// result is obsolete — resolve it so a completed goal never shows open work.
|
|
121
|
-
function resolveBlockingTasks(db, projectId, goalId) {
|
|
123
|
+
async function resolveBlockingTasks(db, projectId, goalId) {
|
|
122
124
|
const marker = acceptanceBlockMarker(goalId);
|
|
123
|
-
for (const task of listTasks(db, projectId)) {
|
|
125
|
+
for (const task of await listTasks(db, projectId)) {
|
|
124
126
|
if (task.goalId === goalId && task.status !== 'done' && task.description?.includes(marker)) {
|
|
125
|
-
updateTask(db, task.id, { status: 'done' });
|
|
127
|
+
await updateTask(db, task.id, { status: 'done' });
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
130
|
}
|
|
@@ -139,15 +141,12 @@ function buildBlockingTaskDescription(goalId, surfaceKind, detail) {
|
|
|
139
141
|
}
|
|
140
142
|
return lines.join('\n');
|
|
141
143
|
}
|
|
142
|
-
function cycleTasksForGoal(db, projectId, goalId) {
|
|
143
|
-
const tagsByTask = listTagsByTaskForProject(db, projectId);
|
|
144
|
-
return listTasks(db, projectId)
|
|
144
|
+
async function cycleTasksForGoal(db, projectId, goalId) {
|
|
145
|
+
const tagsByTask = await listTagsByTaskForProject(db, projectId);
|
|
146
|
+
return (await listTasks(db, projectId))
|
|
145
147
|
.filter((task) => task.goalId === goalId)
|
|
146
148
|
.map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
|
|
147
149
|
}
|
|
148
|
-
function emitGoalUpdated(eventBus, goalId, projectId) {
|
|
149
|
-
eventBus.emit({ type: 'goal_updated', goalId, projectId });
|
|
150
|
-
}
|
|
151
150
|
function recordLastVerification(at, green, kind, detail) {
|
|
152
151
|
const record = { at, green, kind };
|
|
153
152
|
if (detail) {
|
|
@@ -156,18 +155,24 @@ function recordLastVerification(at, green, kind, detail) {
|
|
|
156
155
|
return JSON.stringify(record);
|
|
157
156
|
}
|
|
158
157
|
export function createGoalService(deps) {
|
|
159
|
-
const { db
|
|
158
|
+
const { db } = deps;
|
|
160
159
|
return {
|
|
161
|
-
create(projectId, input) {
|
|
160
|
+
async create(projectId, input) {
|
|
161
|
+
assertPermission(deps, 'goal', 'create');
|
|
162
162
|
if (input.status !== undefined && !isGoalStatus(input.status)) {
|
|
163
163
|
throw new InvalidGoalStatusError(input.status);
|
|
164
164
|
}
|
|
165
165
|
validateVerificationSurfaceInput(input.verificationSurface);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
try {
|
|
167
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
169
174
|
}
|
|
170
|
-
const goal = db
|
|
175
|
+
const goal = await withTransaction(db, async (tx) => createGoal(tx, {
|
|
171
176
|
projectId,
|
|
172
177
|
objective: input.objective,
|
|
173
178
|
status: input.status,
|
|
@@ -178,71 +183,76 @@ export function createGoalService(deps) {
|
|
|
178
183
|
stopCondition: input.stopCondition,
|
|
179
184
|
budget: input.budget,
|
|
180
185
|
}));
|
|
181
|
-
emitGoalUpdated(eventBus, goal.id, projectId);
|
|
182
186
|
return serializeGoal(goal);
|
|
183
187
|
},
|
|
184
|
-
get(goalId) {
|
|
185
|
-
const goal = getGoal(db, goalId);
|
|
188
|
+
async get(goalId) {
|
|
189
|
+
const goal = await getGoal(db, goalId);
|
|
186
190
|
if (!goal) {
|
|
187
191
|
return undefined;
|
|
188
192
|
}
|
|
189
193
|
return {
|
|
190
194
|
...serializeGoal(goal),
|
|
191
|
-
cycle_tasks: cycleTasksForGoal(db, goal.projectId, goalId),
|
|
195
|
+
cycle_tasks: await cycleTasksForGoal(db, goal.projectId, goalId),
|
|
192
196
|
};
|
|
193
197
|
},
|
|
194
|
-
listByProject(projectId) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
async listByProject(projectId) {
|
|
199
|
+
try {
|
|
200
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
198
207
|
}
|
|
199
|
-
return listGoals(db, projectId).map(serializeGoal);
|
|
208
|
+
return (await listGoals(db, projectId)).map(serializeGoal);
|
|
200
209
|
},
|
|
201
|
-
update(goalId, input) {
|
|
202
|
-
|
|
210
|
+
async update(goalId, input) {
|
|
211
|
+
assertPermission(deps, 'goal', 'update');
|
|
212
|
+
const existing = await getGoal(db, goalId);
|
|
203
213
|
if (!existing) {
|
|
204
214
|
return undefined;
|
|
205
215
|
}
|
|
206
216
|
validateVerificationSurfaceInput(input.verificationSurface);
|
|
207
|
-
const goal = db
|
|
217
|
+
const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, input));
|
|
208
218
|
if (!goal) {
|
|
209
219
|
return undefined;
|
|
210
220
|
}
|
|
211
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
212
221
|
return serializeGoal(goal);
|
|
213
222
|
},
|
|
214
|
-
pause(goalId) {
|
|
215
|
-
|
|
223
|
+
async pause(goalId) {
|
|
224
|
+
assertPermission(deps, 'goal', 'update');
|
|
225
|
+
const existing = await getGoal(db, goalId);
|
|
216
226
|
if (!existing) {
|
|
217
227
|
return undefined;
|
|
218
228
|
}
|
|
219
229
|
if (existing.status !== 'active') {
|
|
220
230
|
throw new InvalidGoalTransitionError('Goal can only be paused from active status');
|
|
221
231
|
}
|
|
222
|
-
const goal = db
|
|
232
|
+
const goal = await withTransaction(db, async (tx) => updateGoalStatus(tx, goalId, 'paused'));
|
|
223
233
|
if (!goal) {
|
|
224
234
|
return undefined;
|
|
225
235
|
}
|
|
226
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
227
236
|
return serializeGoal(goal);
|
|
228
237
|
},
|
|
229
|
-
resume(goalId) {
|
|
230
|
-
|
|
238
|
+
async resume(goalId) {
|
|
239
|
+
assertPermission(deps, 'goal', 'update');
|
|
240
|
+
const existing = await getGoal(db, goalId);
|
|
231
241
|
if (!existing) {
|
|
232
242
|
return undefined;
|
|
233
243
|
}
|
|
234
244
|
if (existing.status !== 'paused') {
|
|
235
245
|
throw new InvalidGoalTransitionError('Goal can only be resumed from paused status');
|
|
236
246
|
}
|
|
237
|
-
const goal = db
|
|
247
|
+
const goal = await withTransaction(db, async (tx) => updateGoalStatus(tx, goalId, 'active'));
|
|
238
248
|
if (!goal) {
|
|
239
249
|
return undefined;
|
|
240
250
|
}
|
|
241
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
242
251
|
return serializeGoal(goal);
|
|
243
252
|
},
|
|
244
|
-
complete(goalId, evidence) {
|
|
245
|
-
|
|
253
|
+
async complete(goalId, evidence) {
|
|
254
|
+
assertPermission(deps, 'goal', 'update');
|
|
255
|
+
const existing = await getGoal(db, goalId);
|
|
246
256
|
if (!existing) {
|
|
247
257
|
return undefined;
|
|
248
258
|
}
|
|
@@ -250,7 +260,7 @@ export function createGoalService(deps) {
|
|
|
250
260
|
throw new InvalidGoalTransitionError('Goal is already complete');
|
|
251
261
|
}
|
|
252
262
|
if (existing.status !== 'blocked') {
|
|
253
|
-
const incomplete = listTasks(db, existing.projectId)
|
|
263
|
+
const incomplete = (await listTasks(db, existing.projectId))
|
|
254
264
|
.filter((task) => task.goalId === goalId && task.status !== 'done')
|
|
255
265
|
.map((task) => task.id);
|
|
256
266
|
if (incomplete.length > 0) {
|
|
@@ -260,14 +270,13 @@ export function createGoalService(deps) {
|
|
|
260
270
|
const surface = parseVerificationSurface(existing.verificationSurface);
|
|
261
271
|
const at = new Date().toISOString();
|
|
262
272
|
if (!surface) {
|
|
263
|
-
const goal = db
|
|
273
|
+
const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, {
|
|
264
274
|
status: 'complete',
|
|
265
275
|
lastVerification: recordLastVerification(at, true, null),
|
|
266
276
|
}));
|
|
267
277
|
if (!goal) {
|
|
268
278
|
return undefined;
|
|
269
279
|
}
|
|
270
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
271
280
|
return serializeGoal(goal);
|
|
272
281
|
}
|
|
273
282
|
if (!evidence || evidence.kind !== surface.kind) {
|
|
@@ -276,33 +285,32 @@ export function createGoalService(deps) {
|
|
|
276
285
|
const evaluation = evaluateEvidence(surface, evidence);
|
|
277
286
|
const lastVerification = recordLastVerification(at, evaluation.green, surface.kind, evaluation.detail);
|
|
278
287
|
if (evaluation.green) {
|
|
279
|
-
const goal = db
|
|
280
|
-
const updated = updateGoal(tx, goalId, {
|
|
288
|
+
const goal = await withTransaction(db, async (tx) => {
|
|
289
|
+
const updated = await updateGoal(tx, goalId, {
|
|
281
290
|
status: 'complete',
|
|
282
291
|
lastVerification,
|
|
283
292
|
});
|
|
284
293
|
if (!updated) {
|
|
285
294
|
return undefined;
|
|
286
295
|
}
|
|
287
|
-
resolveBlockingTasks(tx, existing.projectId, goalId);
|
|
296
|
+
await resolveBlockingTasks(tx, existing.projectId, goalId);
|
|
288
297
|
return updated;
|
|
289
298
|
});
|
|
290
299
|
if (!goal) {
|
|
291
300
|
return undefined;
|
|
292
301
|
}
|
|
293
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
294
302
|
return serializeGoal(goal);
|
|
295
303
|
}
|
|
296
|
-
const goal = db
|
|
297
|
-
const updated = updateGoal(tx, goalId, {
|
|
304
|
+
const goal = await withTransaction(db, async (tx) => {
|
|
305
|
+
const updated = await updateGoal(tx, goalId, {
|
|
298
306
|
status: 'blocked',
|
|
299
307
|
lastVerification,
|
|
300
308
|
});
|
|
301
309
|
if (!updated) {
|
|
302
310
|
return undefined;
|
|
303
311
|
}
|
|
304
|
-
if (!hasUnresolvedBlockingTask(tx, existing.projectId, goalId)) {
|
|
305
|
-
createTask(tx, {
|
|
312
|
+
if (!(await hasUnresolvedBlockingTask(tx, existing.projectId, goalId))) {
|
|
313
|
+
await createTask(tx, {
|
|
306
314
|
projectId: existing.projectId,
|
|
307
315
|
goalId,
|
|
308
316
|
label: `Fix acceptance failure: ${truncateObjective(existing.objective)}`,
|
|
@@ -315,7 +323,6 @@ export function createGoalService(deps) {
|
|
|
315
323
|
if (!goal) {
|
|
316
324
|
return undefined;
|
|
317
325
|
}
|
|
318
|
-
emitGoalUpdated(eventBus, goalId, existing.projectId);
|
|
319
326
|
return serializeGoal(goal);
|
|
320
327
|
},
|
|
321
328
|
};
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Db } from '@plandesk/db';
|
|
2
|
-
import { type EventBus } from '../events.js';
|
|
3
2
|
import { type StorageAdapter } from '../storage/index.js';
|
|
4
3
|
import { type CanvasService } from './canvas.js';
|
|
5
4
|
import { type CommentService } from './comments.js';
|
|
@@ -13,16 +12,15 @@ import { type AgentRunService } from './agent-runs.js';
|
|
|
13
12
|
import { type TagService } from './tags.js';
|
|
14
13
|
import { type GoalService } from './goals.js';
|
|
15
14
|
import { type TaskService } from './tasks.js';
|
|
16
|
-
import { type TokenService } from './tokens.js';
|
|
17
15
|
import { type ShareService } from './share.js';
|
|
18
16
|
import { type SyncService } from './sync.js';
|
|
19
17
|
export type ServicesDeps = {
|
|
20
18
|
db: Db;
|
|
21
|
-
eventBus?: EventBus;
|
|
22
19
|
storage?: StorageAdapter;
|
|
20
|
+
/** Fixed org scope for unit tests; production request path uses auth context. */
|
|
21
|
+
orgId?: string;
|
|
23
22
|
};
|
|
24
23
|
export type Services = {
|
|
25
|
-
eventBus: EventBus;
|
|
26
24
|
projectService: ProjectService;
|
|
27
25
|
goalService: GoalService;
|
|
28
26
|
taskService: TaskService;
|
|
@@ -33,7 +31,6 @@ export type Services = {
|
|
|
33
31
|
noteService: NoteService;
|
|
34
32
|
commentService: CommentService;
|
|
35
33
|
agentRunService: AgentRunService;
|
|
36
|
-
tokenService: TokenService;
|
|
37
34
|
shareService: ShareService;
|
|
38
35
|
syncService: SyncService;
|
|
39
36
|
fileService: FileService;
|
package/dist/services/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createEventBus } from '../events.js';
|
|
2
1
|
import { createStorageAdapter } from '../storage/index.js';
|
|
3
2
|
import { createCanvasService } from './canvas.js';
|
|
4
3
|
import { createCommentService } from './comments.js';
|
|
@@ -12,29 +11,26 @@ import { createAgentRunService } from './agent-runs.js';
|
|
|
12
11
|
import { createTagService } from './tags.js';
|
|
13
12
|
import { createGoalService } from './goals.js';
|
|
14
13
|
import { createTaskService } from './tasks.js';
|
|
15
|
-
import { createTokenService } from './tokens.js';
|
|
16
14
|
import { createShareService } from './share.js';
|
|
17
15
|
import { createSyncService } from './sync.js';
|
|
18
16
|
export function createServices(deps) {
|
|
19
|
-
const
|
|
17
|
+
const scoped = { db: deps.db, orgId: deps.orgId };
|
|
20
18
|
const storage = deps.storage ?? createStorageAdapter({ db: deps.db });
|
|
21
|
-
const projectService = createProjectService(
|
|
22
|
-
const goalService = createGoalService(
|
|
23
|
-
const taskService = createTaskService(
|
|
24
|
-
const tagService = createTagService(
|
|
25
|
-
const canvasService = createCanvasService(
|
|
26
|
-
const documentService = createDocumentService(
|
|
27
|
-
const folderService = createFolderService(
|
|
28
|
-
const noteService = createNoteService(
|
|
29
|
-
const commentService = createCommentService(
|
|
30
|
-
const agentRunService = createAgentRunService(
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const artifactService = createArtifactService({ db: deps.db, eventBus });
|
|
19
|
+
const projectService = createProjectService(scoped);
|
|
20
|
+
const goalService = createGoalService(scoped);
|
|
21
|
+
const taskService = createTaskService(scoped);
|
|
22
|
+
const tagService = createTagService(scoped);
|
|
23
|
+
const canvasService = createCanvasService(scoped);
|
|
24
|
+
const documentService = createDocumentService(scoped);
|
|
25
|
+
const folderService = createFolderService(scoped);
|
|
26
|
+
const noteService = createNoteService(scoped);
|
|
27
|
+
const commentService = createCommentService(scoped);
|
|
28
|
+
const agentRunService = createAgentRunService(scoped);
|
|
29
|
+
const shareService = createShareService(scoped);
|
|
30
|
+
const syncService = createSyncService({ ...scoped, taskService });
|
|
31
|
+
const fileService = createFileService({ ...scoped, storage });
|
|
32
|
+
const artifactService = createArtifactService(scoped);
|
|
36
33
|
return {
|
|
37
|
-
eventBus,
|
|
38
34
|
projectService,
|
|
39
35
|
goalService,
|
|
40
36
|
taskService,
|
|
@@ -45,7 +41,6 @@ export function createServices(deps) {
|
|
|
45
41
|
noteService,
|
|
46
42
|
commentService,
|
|
47
43
|
agentRunService,
|
|
48
|
-
tokenService,
|
|
49
44
|
shareService,
|
|
50
45
|
syncService,
|
|
51
46
|
fileService,
|
package/dist/services/notes.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { type Db } from '@plandesk/db';
|
|
2
2
|
import { type PaginationParams, type SerializedNote } from '../serialize.js';
|
|
3
|
-
import type
|
|
4
|
-
export type NoteServiceDeps = {
|
|
3
|
+
import { type OrgScopedDeps } from './org-scope.js';
|
|
4
|
+
export type NoteServiceDeps = OrgScopedDeps & {
|
|
5
5
|
db: Db;
|
|
6
|
-
eventBus: EventBus;
|
|
7
6
|
};
|
|
8
7
|
export type CreateNoteInput = {
|
|
9
8
|
title: string;
|
|
@@ -17,11 +16,11 @@ export declare class InvalidNoteError extends Error {
|
|
|
17
16
|
constructor(message: string);
|
|
18
17
|
}
|
|
19
18
|
export declare function createNoteService(deps: NoteServiceDeps): {
|
|
20
|
-
list(projectId: string, pagination?: PaginationParams): SerializedNote[] | undefined
|
|
21
|
-
create(projectId: string, input: CreateNoteInput): SerializedNote | undefined
|
|
22
|
-
get(id: string): SerializedNote | undefined
|
|
23
|
-
update(id: string, input: UpdateNoteInput): SerializedNote | undefined
|
|
24
|
-
delete(id: string): boolean
|
|
19
|
+
list(projectId: string, pagination?: PaginationParams): Promise<SerializedNote[] | undefined>;
|
|
20
|
+
create(projectId: string, input: CreateNoteInput): Promise<SerializedNote | undefined>;
|
|
21
|
+
get(id: string): Promise<SerializedNote | undefined>;
|
|
22
|
+
update(id: string, input: UpdateNoteInput): Promise<SerializedNote | undefined>;
|
|
23
|
+
delete(id: string): Promise<boolean>;
|
|
25
24
|
};
|
|
26
25
|
export type NoteService = ReturnType<typeof createNoteService>;
|
|
27
26
|
//# sourceMappingURL=notes.d.ts.map
|
package/dist/services/notes.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createNote as dbCreateNote, deleteCommentsByTarget, deleteNote as dbDeleteNote, getNote as dbGetNote, getProject, listNotes as dbListNotes, updateNote as dbUpdateNote, } from '@plandesk/db';
|
|
2
2
|
import { serializeNote } from '../serialize.js';
|
|
3
|
+
import { assertPermission, resolveOrgId } from './org-scope.js';
|
|
4
|
+
import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
|
|
3
5
|
export class InvalidNoteError extends Error {
|
|
4
6
|
constructor(message) {
|
|
5
7
|
super(message);
|
|
@@ -12,62 +14,72 @@ function assertNonEmptyTitle(title) {
|
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
export function createNoteService(deps) {
|
|
15
|
-
const { db
|
|
17
|
+
const { db } = deps;
|
|
16
18
|
return {
|
|
17
|
-
list(projectId, pagination = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
async list(projectId, pagination = {}) {
|
|
20
|
+
try {
|
|
21
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
throw error;
|
|
21
28
|
}
|
|
22
|
-
return dbListNotes(db, projectId, pagination).map(serializeNote);
|
|
29
|
+
return (await dbListNotes(db, projectId, pagination)).map(serializeNote);
|
|
23
30
|
},
|
|
24
|
-
create(projectId, input) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
async create(projectId, input) {
|
|
32
|
+
assertPermission(deps, 'document', 'create');
|
|
33
|
+
try {
|
|
34
|
+
await assertProjectInOrg(db, projectId, resolveOrgId(deps));
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error instanceof ProjectNotInOrgError) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
28
41
|
}
|
|
29
42
|
assertNonEmptyTitle(input.title);
|
|
30
|
-
const note = dbCreateNote(db, {
|
|
43
|
+
const note = await dbCreateNote(db, {
|
|
31
44
|
projectId,
|
|
32
45
|
title: input.title,
|
|
33
46
|
body: input.body,
|
|
34
47
|
});
|
|
35
|
-
eventBus.emit({ type: 'note_created', noteId: note.id, projectId });
|
|
36
48
|
return serializeNote(note);
|
|
37
49
|
},
|
|
38
|
-
get(id) {
|
|
39
|
-
const note = dbGetNote(db, id);
|
|
50
|
+
async get(id) {
|
|
51
|
+
const note = await dbGetNote(db, id);
|
|
40
52
|
if (!note) {
|
|
41
53
|
return undefined;
|
|
42
54
|
}
|
|
43
55
|
return serializeNote(note);
|
|
44
56
|
},
|
|
45
|
-
update(id, input) {
|
|
46
|
-
|
|
57
|
+
async update(id, input) {
|
|
58
|
+
assertPermission(deps, 'document', 'update');
|
|
59
|
+
const existing = await dbGetNote(db, id);
|
|
47
60
|
if (!existing) {
|
|
48
61
|
return undefined;
|
|
49
62
|
}
|
|
50
63
|
if (input.title !== undefined) {
|
|
51
64
|
assertNonEmptyTitle(input.title);
|
|
52
65
|
}
|
|
53
|
-
const note = dbUpdateNote(db, id, input);
|
|
66
|
+
const note = await dbUpdateNote(db, id, input);
|
|
54
67
|
if (!note) {
|
|
55
68
|
return undefined;
|
|
56
69
|
}
|
|
57
|
-
eventBus.emit({ type: 'note_updated', noteId: note.id, projectId: note.projectId });
|
|
58
70
|
return serializeNote(note);
|
|
59
71
|
},
|
|
60
|
-
delete(id) {
|
|
61
|
-
|
|
72
|
+
async delete(id) {
|
|
73
|
+
assertPermission(deps, 'document', 'delete');
|
|
74
|
+
const existing = await dbGetNote(db, id);
|
|
62
75
|
if (!existing) {
|
|
63
76
|
return false;
|
|
64
77
|
}
|
|
65
|
-
const deleted = dbDeleteNote(db, id);
|
|
78
|
+
const deleted = await dbDeleteNote(db, id);
|
|
66
79
|
if (!deleted) {
|
|
67
80
|
return false;
|
|
68
81
|
}
|
|
69
|
-
deleteCommentsByTarget(db, 'note', id);
|
|
70
|
-
eventBus.emit({ type: 'note_updated', noteId: id, projectId: existing.projectId });
|
|
82
|
+
await deleteCommentsByTarget(db, 'note', id);
|
|
71
83
|
return true;
|
|
72
84
|
},
|
|
73
85
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OrgRole } from '@plandesk/db';
|
|
2
|
+
import { type PermissionSet } from '../permissions.js';
|
|
3
|
+
export type OrgScopedDeps = {
|
|
4
|
+
/** Fixed org scope for unit tests; production uses request auth context. */
|
|
5
|
+
orgId?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Fixed permission for unit tests. Production uses AuthContext.permission.
|
|
8
|
+
* When orgId is injected without permission (legacy unit-test path), defaults
|
|
9
|
+
* to owner so role checks do not require a full request context.
|
|
10
|
+
*/
|
|
11
|
+
permission?: OrgRole | PermissionSet;
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveOrgId(deps: OrgScopedDeps): string;
|
|
14
|
+
/**
|
|
15
|
+
* Effective permission set for this call.
|
|
16
|
+
* Priority: deps.permission → AuthContext.permission → owner (service unit tests
|
|
17
|
+
* that invoke methods without request middleware; production always has context).
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolvePermissionSet(deps: OrgScopedDeps): PermissionSet;
|
|
20
|
+
/** requirePermission against the caller's effective permission set (deps or AuthContext). */
|
|
21
|
+
export declare function assertPermission(deps: OrgScopedDeps, resource: string, action: string): void;
|
|
22
|
+
//# sourceMappingURL=org-scope.d.ts.map
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getAuthContext, tryGetAuthContext } from '../auth-context.js';
|
|
2
|
+
import { orgRoleToPermissionSet, requirePermission, } from '../permissions.js';
|
|
3
|
+
export function resolveOrgId(deps) {
|
|
4
|
+
if (deps.orgId !== undefined) {
|
|
5
|
+
return deps.orgId;
|
|
6
|
+
}
|
|
7
|
+
const ctx = getAuthContext();
|
|
8
|
+
if (ctx.kind === 'guest') {
|
|
9
|
+
throw new Error('Guest context has no orgId');
|
|
10
|
+
}
|
|
11
|
+
return ctx.orgId;
|
|
12
|
+
}
|
|
13
|
+
function isOrgRole(value) {
|
|
14
|
+
return typeof value === 'string';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Effective permission set for this call.
|
|
18
|
+
* Priority: deps.permission → AuthContext.permission → owner (service unit tests
|
|
19
|
+
* that invoke methods without request middleware; production always has context).
|
|
20
|
+
*/
|
|
21
|
+
export function resolvePermissionSet(deps) {
|
|
22
|
+
if (deps.permission !== undefined) {
|
|
23
|
+
return isOrgRole(deps.permission) ? orgRoleToPermissionSet(deps.permission) : deps.permission;
|
|
24
|
+
}
|
|
25
|
+
const ctx = tryGetAuthContext();
|
|
26
|
+
if (ctx !== undefined && ctx.kind !== 'guest') {
|
|
27
|
+
return ctx.permission;
|
|
28
|
+
}
|
|
29
|
+
if (ctx?.kind === 'guest') {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
return orgRoleToPermissionSet('owner');
|
|
33
|
+
}
|
|
34
|
+
/** requirePermission against the caller's effective permission set (deps or AuthContext). */
|
|
35
|
+
export function assertPermission(deps, resource, action) {
|
|
36
|
+
requirePermission({ permission: resolvePermissionSet(deps) }, resource, action);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=org-scope.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Db, type TaskStatus } from '@plandesk/db';
|
|
2
|
-
import type { EventBus } from '../events.js';
|
|
3
2
|
import { serializeEdge, serializeProject, serializeTask, type PaginationParams, type SerializedDocument, type TaskStatusSummary } from '../serialize.js';
|
|
3
|
+
import { type OrgScopedDeps } from './org-scope.js';
|
|
4
4
|
type SerializedProject = ReturnType<typeof serializeProject>;
|
|
5
5
|
type SerializedTask = ReturnType<typeof serializeTask>;
|
|
6
6
|
type SerializedEdge = ReturnType<typeof serializeEdge>;
|
|
@@ -49,9 +49,8 @@ export type ScaffoldPlanResult = {
|
|
|
49
49
|
documents: number;
|
|
50
50
|
};
|
|
51
51
|
};
|
|
52
|
-
export type ProjectServiceDeps = {
|
|
52
|
+
export type ProjectServiceDeps = OrgScopedDeps & {
|
|
53
53
|
db: Db;
|
|
54
|
-
eventBus: EventBus;
|
|
55
54
|
};
|
|
56
55
|
export type CreateProjectInput = {
|
|
57
56
|
name: string;
|
|
@@ -62,37 +61,37 @@ export type UpdateProjectInput = {
|
|
|
62
61
|
description?: string | null;
|
|
63
62
|
};
|
|
64
63
|
export declare function createProjectService(deps: ProjectServiceDeps): {
|
|
65
|
-
create(input: CreateProjectInput): {
|
|
64
|
+
create(input: CreateProjectInput): Promise<{
|
|
66
65
|
id: string;
|
|
67
66
|
name: string;
|
|
68
67
|
description: string | null;
|
|
69
68
|
created_at: string;
|
|
70
69
|
updated_at: string;
|
|
71
|
-
}
|
|
72
|
-
list(pagination?: PaginationParams): {
|
|
70
|
+
}>;
|
|
71
|
+
list(pagination?: PaginationParams): Promise<{
|
|
73
72
|
id: string;
|
|
74
73
|
name: string;
|
|
75
74
|
description: string | null;
|
|
76
75
|
created_at: string;
|
|
77
76
|
updated_at: string;
|
|
78
|
-
}[]
|
|
79
|
-
get(id: string): {
|
|
77
|
+
}[]>;
|
|
78
|
+
get(id: string): Promise<{
|
|
80
79
|
summary: TaskStatusSummary;
|
|
81
80
|
id: string;
|
|
82
81
|
name: string;
|
|
83
82
|
description: string | null;
|
|
84
83
|
created_at: string;
|
|
85
84
|
updated_at: string;
|
|
86
|
-
} | undefined
|
|
87
|
-
update(id: string, input: UpdateProjectInput): {
|
|
85
|
+
} | undefined>;
|
|
86
|
+
update(id: string, input: UpdateProjectInput): Promise<{
|
|
88
87
|
id: string;
|
|
89
88
|
name: string;
|
|
90
89
|
description: string | null;
|
|
91
90
|
created_at: string;
|
|
92
91
|
updated_at: string;
|
|
93
|
-
} | undefined
|
|
94
|
-
delete(id: string): boolean
|
|
95
|
-
scaffoldFromPlan(input: ScaffoldPlanInput): ScaffoldPlanResult
|
|
92
|
+
} | undefined>;
|
|
93
|
+
delete(id: string): Promise<boolean>;
|
|
94
|
+
scaffoldFromPlan(input: ScaffoldPlanInput): Promise<ScaffoldPlanResult>;
|
|
96
95
|
};
|
|
97
96
|
export type ProjectService = ReturnType<typeof createProjectService>;
|
|
98
97
|
export {};
|