@agent-native/core 0.114.5 → 0.114.6
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/client/resources/ResourceTree.tsx +11 -5
- package/corpus/core/src/client/resources/ResourcesPanel.tsx +141 -18
- package/corpus/templates/clips/app/components/meetings/meeting-card.tsx +1 -2
- package/corpus/templates/clips/changelog/2026-07-20-opening-zoom-meeting-launches-native-app.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +7 -11
- package/corpus/templates/clips/desktop/src/lib/open-meeting-join-url.ts +22 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +2 -3
- package/corpus/templates/clips/shared/meeting-join-url.ts +0 -5
- package/corpus/templates/content/.agents/skills/document-editing/SKILL.md +9 -1
- package/corpus/templates/content/AGENTS.md +4 -1
- package/corpus/templates/content/actions/_content-spaces.ts +6 -1
- package/corpus/templates/content/actions/_database-row-batch.ts +15 -2
- package/corpus/templates/content/actions/_database-utils.ts +83 -15
- package/corpus/templates/content/actions/add-database-item.ts +3 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +2 -1
- package/corpus/templates/content/actions/delete-content-database.ts +12 -9
- package/corpus/templates/content/actions/delete-database-items.ts +3 -2
- package/corpus/templates/content/actions/delete-document.ts +233 -6
- package/corpus/templates/content/actions/duplicate-database-item.ts +1 -0
- package/corpus/templates/content/actions/get-document.ts +4 -1
- package/corpus/templates/content/actions/list-content-databases.ts +1 -0
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/actions/list-trashed-content-databases.ts +8 -2
- package/corpus/templates/content/actions/list-trashed-documents.ts +50 -0
- package/corpus/templates/content/actions/permanently-delete-document.ts +28 -0
- package/corpus/templates/content/actions/pull-document.ts +15 -2
- package/corpus/templates/content/actions/restore-content-database.ts +48 -11
- package/corpus/templates/content/actions/restore-document.ts +28 -0
- package/corpus/templates/content/actions/search-documents.ts +2 -1
- package/corpus/templates/content/actions/view-screen.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +149 -55
- package/corpus/templates/content/app/components/editor/database/GalleryView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/ListView.tsx +7 -2
- package/corpus/templates/content/app/components/editor/database/TimelineView.tsx +3 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +26 -5
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +243 -163
- package/corpus/templates/content/app/components/sidebar/WorkspaceSourceMenu.tsx +154 -0
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +11 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +6 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +57 -0
- package/corpus/templates/content/app/i18n-data.ts +83 -2
- package/corpus/templates/content/app/routes/_app.local-files.tsx +8 -1
- package/corpus/templates/content/changelog/2026-07-19-pages-now-move-to-a-reversible-trash-and-the-organization-pi.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-trash-actions-now-preserve-independent-archived-pages-enforc.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspace-toggles-now-stay-independently-open-or-closed-and-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-20-workspaces-can-be-added-from-a-blank-workspace-or-a-connecte.md +6 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +3 -0
- package/corpus/templates/content/server/db/schema.ts +2 -0
- package/corpus/templates/content/server/lib/public-documents.ts +8 -2
- package/corpus/templates/content/server/plugins/db.ts +52 -0
- package/corpus/templates/content/server/routes/api/document-agent-context.json.get.ts +2 -2
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/ui/progress.tsx +3 -2
- package/dist/client/resources/ResourceTree.d.ts +3 -1
- package/dist/client/resources/ResourceTree.d.ts.map +1 -1
- package/dist/client/resources/ResourceTree.js +4 -4
- package/dist/client/resources/ResourceTree.js.map +1 -1
- package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
- package/dist/client/resources/ResourcesPanel.js +55 -6
- package/dist/client/resources/ResourcesPanel.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/client/resources/ResourceTree.tsx +11 -5
- package/src/client/resources/ResourcesPanel.tsx +141 -18
|
@@ -22,7 +22,10 @@ import type {
|
|
|
22
22
|
ContentDatabaseResponse,
|
|
23
23
|
} from "../shared/api.js";
|
|
24
24
|
import { favoriteDocumentIds } from "./_content-favorites.js";
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
listContentOrganizationMemberships,
|
|
27
|
+
normalizeContentSpaceEmail,
|
|
28
|
+
} from "./_content-space-access.js";
|
|
26
29
|
import { getAllContentDatabaseSourceSnapshots } from "./_database-source-utils.js";
|
|
27
30
|
import {
|
|
28
31
|
applyFederatedOverlayValues,
|
|
@@ -81,6 +84,8 @@ export const contentDatabaseListDocumentSelection = {
|
|
|
81
84
|
sourcePath: schema.documents.sourcePath,
|
|
82
85
|
sourceRootPath: schema.documents.sourceRootPath,
|
|
83
86
|
sourceUpdatedAt: schema.documents.sourceUpdatedAt,
|
|
87
|
+
trashedAt: schema.documents.trashedAt,
|
|
88
|
+
trashRootId: schema.documents.trashRootId,
|
|
84
89
|
visibility: schema.documents.visibility,
|
|
85
90
|
ownerEmail: schema.documents.ownerEmail,
|
|
86
91
|
orgId: schema.documents.orgId,
|
|
@@ -300,9 +305,14 @@ export async function getContentDatabaseResponse(
|
|
|
300
305
|
|
|
301
306
|
const { limit, offset } = normalizeContentDatabasePageOptions(options);
|
|
302
307
|
const userEmail = getRequestUserEmail();
|
|
308
|
+
const normalizedUserEmail = userEmail
|
|
309
|
+
? normalizeContentSpaceEmail(userEmail)
|
|
310
|
+
: null;
|
|
303
311
|
const activeOrgId = getRequestOrgId();
|
|
304
312
|
const authorizedOrgIds =
|
|
305
|
-
database.systemRole === "favorites"
|
|
313
|
+
(database.systemRole === "favorites" ||
|
|
314
|
+
database.systemRole === "workspaces") &&
|
|
315
|
+
userEmail
|
|
306
316
|
? [
|
|
307
317
|
...new Set([
|
|
308
318
|
...(await listContentOrganizationMemberships(userEmail)).map(
|
|
@@ -312,6 +322,45 @@ export async function getContentDatabaseResponse(
|
|
|
312
322
|
]),
|
|
313
323
|
]
|
|
314
324
|
: [];
|
|
325
|
+
const workspacesVisibleDocumentIds =
|
|
326
|
+
database.systemRole === "workspaces" && normalizedUserEmail
|
|
327
|
+
? (
|
|
328
|
+
await db
|
|
329
|
+
.select({
|
|
330
|
+
documentId: schema.contentSpaceCatalogItems.documentId,
|
|
331
|
+
ownerEmail: schema.contentSpaces.ownerEmail,
|
|
332
|
+
orgId: schema.contentSpaces.orgId,
|
|
333
|
+
})
|
|
334
|
+
.from(schema.contentSpaceCatalogItems)
|
|
335
|
+
.innerJoin(
|
|
336
|
+
schema.contentSpaces,
|
|
337
|
+
eq(
|
|
338
|
+
schema.contentSpaces.id,
|
|
339
|
+
schema.contentSpaceCatalogItems.spaceId,
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
.where(
|
|
343
|
+
and(
|
|
344
|
+
eq(
|
|
345
|
+
schema.contentSpaceCatalogItems.catalogDatabaseId,
|
|
346
|
+
databaseId,
|
|
347
|
+
),
|
|
348
|
+
eq(
|
|
349
|
+
schema.contentSpaceCatalogItems.ownerEmail,
|
|
350
|
+
normalizedUserEmail,
|
|
351
|
+
),
|
|
352
|
+
isNull(schema.contentSpaces.archivedAt),
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
.filter(
|
|
357
|
+
(row) =>
|
|
358
|
+
normalizeContentSpaceEmail(row.ownerEmail) ===
|
|
359
|
+
normalizedUserEmail ||
|
|
360
|
+
(!!row.orgId && authorizedOrgIds.includes(row.orgId)),
|
|
361
|
+
)
|
|
362
|
+
.map((row) => row.documentId)
|
|
363
|
+
: null;
|
|
315
364
|
const favoritesVisibleDocumentIds =
|
|
316
365
|
database.systemRole === "favorites" && userEmail
|
|
317
366
|
? (
|
|
@@ -331,6 +380,7 @@ export async function getContentDatabaseResponse(
|
|
|
331
380
|
}),
|
|
332
381
|
),
|
|
333
382
|
),
|
|
383
|
+
isNull(schema.documents.trashedAt),
|
|
334
384
|
documentDiscoveryFilter({
|
|
335
385
|
userEmail,
|
|
336
386
|
orgIds: authorizedOrgIds,
|
|
@@ -351,6 +401,11 @@ export async function getContentDatabaseResponse(
|
|
|
351
401
|
: undefined;
|
|
352
402
|
const visibleItemFilter = and(
|
|
353
403
|
eq(schema.contentDatabaseItems.databaseId, databaseId),
|
|
404
|
+
sql`exists (
|
|
405
|
+
select 1 from ${schema.documents}
|
|
406
|
+
where ${schema.documents.id} = ${schema.contentDatabaseItems.documentId}
|
|
407
|
+
and ${schema.documents.trashedAt} is null
|
|
408
|
+
)`,
|
|
354
409
|
organizationFilesItemFilter,
|
|
355
410
|
favoritesVisibleDocumentIds
|
|
356
411
|
? favoritesVisibleDocumentIds.length > 0
|
|
@@ -360,6 +415,14 @@ export async function getContentDatabaseResponse(
|
|
|
360
415
|
)
|
|
361
416
|
: sql`1 = 0`
|
|
362
417
|
: undefined,
|
|
418
|
+
workspacesVisibleDocumentIds
|
|
419
|
+
? workspacesVisibleDocumentIds.length > 0
|
|
420
|
+
? inArray(
|
|
421
|
+
schema.contentDatabaseItems.documentId,
|
|
422
|
+
workspacesVisibleDocumentIds,
|
|
423
|
+
)
|
|
424
|
+
: sql`1 = 0`
|
|
425
|
+
: undefined,
|
|
363
426
|
);
|
|
364
427
|
const [itemCount] = await db
|
|
365
428
|
.select({ count: sql<number>`COUNT(*)` })
|
|
@@ -388,23 +451,28 @@ export async function getContentDatabaseResponse(
|
|
|
388
451
|
schema.documents.id,
|
|
389
452
|
items.map((item) => item.documentId),
|
|
390
453
|
),
|
|
454
|
+
isNull(schema.documents.trashedAt),
|
|
391
455
|
database.systemRole === "favorites"
|
|
392
456
|
? favoritesVisibleDocumentIds?.length
|
|
393
457
|
? inArray(schema.documents.id, favoritesVisibleDocumentIds)
|
|
394
458
|
: sql`1 = 0`
|
|
395
|
-
: database.systemRole === "
|
|
396
|
-
?
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
459
|
+
: database.systemRole === "workspaces"
|
|
460
|
+
? workspacesVisibleDocumentIds?.length
|
|
461
|
+
? inArray(schema.documents.id, workspacesVisibleDocumentIds)
|
|
462
|
+
: sql`1 = 0`
|
|
463
|
+
: database.systemRole === "files" && database.orgId
|
|
464
|
+
? and(
|
|
465
|
+
eq(schema.documents.orgId, database.orgId),
|
|
466
|
+
or(
|
|
467
|
+
eq(schema.documents.visibility, "org"),
|
|
468
|
+
eq(schema.documents.visibility, "public"),
|
|
469
|
+
),
|
|
470
|
+
or(
|
|
471
|
+
eq(schema.documents.hideFromSearch, 0),
|
|
472
|
+
isNull(schema.documents.hideFromSearch),
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
: eq(schema.documents.ownerEmail, database.ownerEmail),
|
|
408
476
|
),
|
|
409
477
|
)
|
|
410
478
|
: [];
|
|
@@ -41,6 +41,9 @@ export default defineAction({
|
|
|
41
41
|
),
|
|
42
42
|
);
|
|
43
43
|
if (!database) throw new Error(`Database "${databaseId}" not found`);
|
|
44
|
+
if (database.systemRole === "workspaces") {
|
|
45
|
+
throw new Error("Use create-content-space to add a workspace");
|
|
46
|
+
}
|
|
44
47
|
|
|
45
48
|
const access = await assertAccess(
|
|
46
49
|
"document",
|
|
@@ -51,6 +51,7 @@ export default defineAction({
|
|
|
51
51
|
.optional()
|
|
52
52
|
.default(false)
|
|
53
53
|
.describe("Create a separate private Content space for this folder"),
|
|
54
|
+
propertyValues: z.record(z.string(), z.unknown()).optional(),
|
|
54
55
|
truthPolicy: truthPolicySchema.describe(
|
|
55
56
|
"Whether Content, the folder, or reviewed conflict resolution is authoritative",
|
|
56
57
|
),
|
|
@@ -130,7 +131,7 @@ export default defineAction({
|
|
|
130
131
|
const provisioned = await provisionSourceBackedContentSpace(
|
|
131
132
|
db,
|
|
132
133
|
userEmail,
|
|
133
|
-
{ connectionId, name: label },
|
|
134
|
+
{ connectionId, name: label, propertyValues: args.propertyValues },
|
|
134
135
|
);
|
|
135
136
|
targetSpaceId = provisioned.spaceId;
|
|
136
137
|
targetDatabaseId = provisioned.filesDatabaseId;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
|
-
import {
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
|
|
6
|
-
import { getDb
|
|
6
|
+
import { getDb } from "../server/db/index.js";
|
|
7
7
|
import { assertContentDatabaseLifecycleAccess } from "./_content-database-lifecycle.js";
|
|
8
|
+
import { trashDocumentSubtree } from "./delete-document.js";
|
|
8
9
|
|
|
9
10
|
export default defineAction({
|
|
10
11
|
description:
|
|
@@ -17,15 +18,17 @@ export default defineAction({
|
|
|
17
18
|
if (database.systemRole) {
|
|
18
19
|
throw new Error("System Content databases cannot be deleted");
|
|
19
20
|
}
|
|
21
|
+
await assertAccess("document", database.documentId, "admin");
|
|
20
22
|
const db = getDb();
|
|
21
23
|
const deletedAt = database.deletedAt ?? new Date().toISOString();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
await db.transaction((tx) =>
|
|
25
|
+
trashDocumentSubtree(
|
|
26
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
27
|
+
database.documentId,
|
|
28
|
+
database.ownerEmail,
|
|
29
|
+
deletedAt,
|
|
30
|
+
),
|
|
31
|
+
);
|
|
29
32
|
|
|
30
33
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
31
34
|
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
resolveDatabaseRowsForBatch,
|
|
12
12
|
} from "./_database-row-batch.js";
|
|
13
13
|
import { getContentDatabaseResponse } from "./_database-utils.js";
|
|
14
|
-
import {
|
|
14
|
+
import { trashDocumentSubtree } from "./delete-document.js";
|
|
15
15
|
|
|
16
16
|
export default defineAction({
|
|
17
17
|
description:
|
|
@@ -57,10 +57,11 @@ export default defineAction({
|
|
|
57
57
|
|
|
58
58
|
await db.transaction(async (tx) => {
|
|
59
59
|
for (const row of rows) {
|
|
60
|
-
await
|
|
60
|
+
await trashDocumentSubtree(
|
|
61
61
|
tx as unknown as ReturnType<typeof getDb>,
|
|
62
62
|
row.document.id,
|
|
63
63
|
row.document.ownerEmail,
|
|
64
|
+
now,
|
|
64
65
|
);
|
|
65
66
|
}
|
|
66
67
|
await renumberDatabaseRows(
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
2
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
3
|
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
-
import { and, eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { and, eq, inArray, isNotNull, isNull, ne, or } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
8
8
|
import { chunks } from "./_batch-utils.js";
|
|
9
9
|
import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
|
|
10
|
+
import { renumberDatabaseRows } from "./_database-row-batch.js";
|
|
10
11
|
|
|
11
12
|
const DELETE_BATCH_SIZE = 90;
|
|
12
13
|
|
|
@@ -155,6 +156,152 @@ async function collectDocumentSubtreeForDelete(
|
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
export async function trashDocumentSubtree(
|
|
160
|
+
db: ReturnType<typeof getDb>,
|
|
161
|
+
id: string,
|
|
162
|
+
ownerEmail: string,
|
|
163
|
+
trashedAt = new Date().toISOString(),
|
|
164
|
+
): Promise<string[]> {
|
|
165
|
+
const { documentIds } = await collectDocumentSubtreeForDelete(
|
|
166
|
+
db,
|
|
167
|
+
id,
|
|
168
|
+
ownerEmail,
|
|
169
|
+
);
|
|
170
|
+
await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
|
|
171
|
+
|
|
172
|
+
const independentlyTrashedDatabaseDocumentIds = new Set<string>();
|
|
173
|
+
for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
|
|
174
|
+
for (const database of await db
|
|
175
|
+
.select({ documentId: schema.contentDatabases.documentId })
|
|
176
|
+
.from(schema.contentDatabases)
|
|
177
|
+
.where(
|
|
178
|
+
and(
|
|
179
|
+
inArray(schema.contentDatabases.documentId, batch),
|
|
180
|
+
eq(schema.contentDatabases.ownerEmail, ownerEmail),
|
|
181
|
+
isNotNull(schema.contentDatabases.deletedAt),
|
|
182
|
+
),
|
|
183
|
+
)) {
|
|
184
|
+
independentlyTrashedDatabaseDocumentIds.add(database.documentId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const activeDocumentIds: string[] = [];
|
|
189
|
+
for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
|
|
190
|
+
activeDocumentIds.push(
|
|
191
|
+
...(
|
|
192
|
+
await db
|
|
193
|
+
.select({ id: schema.documents.id })
|
|
194
|
+
.from(schema.documents)
|
|
195
|
+
.where(
|
|
196
|
+
and(
|
|
197
|
+
inArray(schema.documents.id, batch),
|
|
198
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
199
|
+
isNull(schema.documents.trashedAt),
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
.map((document) => document.id)
|
|
204
|
+
.filter(
|
|
205
|
+
(documentId) =>
|
|
206
|
+
!independentlyTrashedDatabaseDocumentIds.has(documentId),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const batch of chunks(activeDocumentIds, DELETE_BATCH_SIZE)) {
|
|
212
|
+
await db
|
|
213
|
+
.update(schema.documents)
|
|
214
|
+
.set({ trashedAt, trashRootId: id, updatedAt: trashedAt })
|
|
215
|
+
.where(
|
|
216
|
+
and(
|
|
217
|
+
inArray(schema.documents.id, batch),
|
|
218
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
219
|
+
isNull(schema.documents.trashedAt),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
await db
|
|
223
|
+
.update(schema.contentDatabases)
|
|
224
|
+
.set({ deletedAt: trashedAt, updatedAt: trashedAt })
|
|
225
|
+
.where(
|
|
226
|
+
and(
|
|
227
|
+
inArray(schema.contentDatabases.documentId, batch),
|
|
228
|
+
eq(schema.contentDatabases.ownerEmail, ownerEmail),
|
|
229
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return activeDocumentIds;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function restoreDocumentSubtree(
|
|
238
|
+
db: ReturnType<typeof getDb>,
|
|
239
|
+
rootId: string,
|
|
240
|
+
ownerEmail: string,
|
|
241
|
+
): Promise<string[]> {
|
|
242
|
+
const documentIds = (
|
|
243
|
+
await db
|
|
244
|
+
.select({ id: schema.documents.id })
|
|
245
|
+
.from(schema.documents)
|
|
246
|
+
.where(
|
|
247
|
+
and(
|
|
248
|
+
eq(schema.documents.trashRootId, rootId),
|
|
249
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
).map((document) => document.id);
|
|
253
|
+
if (documentIds.length === 0) return [];
|
|
254
|
+
|
|
255
|
+
const now = new Date().toISOString();
|
|
256
|
+
for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
|
|
257
|
+
await db
|
|
258
|
+
.update(schema.documents)
|
|
259
|
+
.set({ trashedAt: null, trashRootId: null, updatedAt: now })
|
|
260
|
+
.where(
|
|
261
|
+
and(
|
|
262
|
+
inArray(schema.documents.id, batch),
|
|
263
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
264
|
+
eq(schema.documents.trashRootId, rootId),
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
await db
|
|
268
|
+
.update(schema.contentDatabases)
|
|
269
|
+
.set({ deletedAt: null, updatedAt: now })
|
|
270
|
+
.where(
|
|
271
|
+
and(
|
|
272
|
+
inArray(schema.contentDatabases.documentId, batch),
|
|
273
|
+
eq(schema.contentDatabases.ownerEmail, ownerEmail),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const databaseIds = [
|
|
279
|
+
...new Set(
|
|
280
|
+
(
|
|
281
|
+
await db
|
|
282
|
+
.select({ databaseId: schema.contentDatabaseItems.databaseId })
|
|
283
|
+
.from(schema.contentDatabaseItems)
|
|
284
|
+
.where(inArray(schema.contentDatabaseItems.documentId, documentIds))
|
|
285
|
+
).map((item) => item.databaseId),
|
|
286
|
+
),
|
|
287
|
+
];
|
|
288
|
+
for (const databaseId of databaseIds) {
|
|
289
|
+
const [database] = await db
|
|
290
|
+
.select()
|
|
291
|
+
.from(schema.contentDatabases)
|
|
292
|
+
.where(
|
|
293
|
+
and(
|
|
294
|
+
eq(schema.contentDatabases.id, databaseId),
|
|
295
|
+
isNull(schema.contentDatabases.deletedAt),
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
.limit(1);
|
|
299
|
+
if (database) await renumberDatabaseRows(db, database, now);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return documentIds;
|
|
303
|
+
}
|
|
304
|
+
|
|
158
305
|
async function deleteWhereIn<T>(
|
|
159
306
|
items: T[],
|
|
160
307
|
run: (batch: T[]) => Promise<unknown>,
|
|
@@ -171,6 +318,20 @@ export async function deleteDocumentRecursive(
|
|
|
171
318
|
): Promise<string[]> {
|
|
172
319
|
const { documentIds, ownedDatabaseIds } =
|
|
173
320
|
await collectDocumentSubtreeForDelete(db, id, ownerEmail);
|
|
321
|
+
return deleteCollectedDocuments(
|
|
322
|
+
db,
|
|
323
|
+
documentIds,
|
|
324
|
+
ownedDatabaseIds,
|
|
325
|
+
ownerEmail,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function deleteCollectedDocuments(
|
|
330
|
+
db: ReturnType<typeof getDb>,
|
|
331
|
+
documentIds: string[],
|
|
332
|
+
ownedDatabaseIds: string[],
|
|
333
|
+
ownerEmail: string,
|
|
334
|
+
): Promise<string[]> {
|
|
174
335
|
await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
|
|
175
336
|
|
|
176
337
|
const propertyDefinitionIds: string[] = [];
|
|
@@ -353,8 +514,72 @@ export async function deleteDocumentRecursive(
|
|
|
353
514
|
return documentIds;
|
|
354
515
|
}
|
|
355
516
|
|
|
517
|
+
export async function deleteTrashedDocumentSubtree(
|
|
518
|
+
db: ReturnType<typeof getDb>,
|
|
519
|
+
id: string,
|
|
520
|
+
ownerEmail: string,
|
|
521
|
+
): Promise<string[]> {
|
|
522
|
+
const [root] = await db
|
|
523
|
+
.select({ id: schema.documents.id })
|
|
524
|
+
.from(schema.documents)
|
|
525
|
+
.where(
|
|
526
|
+
and(
|
|
527
|
+
eq(schema.documents.id, id),
|
|
528
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
529
|
+
eq(schema.documents.trashRootId, id),
|
|
530
|
+
isNotNull(schema.documents.trashedAt),
|
|
531
|
+
),
|
|
532
|
+
)
|
|
533
|
+
.limit(1);
|
|
534
|
+
if (!root) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
"Document must be in Trash and be a Trash root before permanent deletion",
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const documentIds = (
|
|
541
|
+
await db
|
|
542
|
+
.select({ id: schema.documents.id })
|
|
543
|
+
.from(schema.documents)
|
|
544
|
+
.where(
|
|
545
|
+
and(
|
|
546
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
547
|
+
eq(schema.documents.trashRootId, id),
|
|
548
|
+
isNotNull(schema.documents.trashedAt),
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
).map((document) => document.id);
|
|
552
|
+
const ownedDatabaseIds = await selectOwnedDatabaseIds(
|
|
553
|
+
db,
|
|
554
|
+
documentIds,
|
|
555
|
+
ownerEmail,
|
|
556
|
+
).then((rows) => rows.map((database) => database.id));
|
|
557
|
+
|
|
558
|
+
await db
|
|
559
|
+
.update(schema.documents)
|
|
560
|
+
.set({ parentId: null, updatedAt: new Date().toISOString() })
|
|
561
|
+
.where(
|
|
562
|
+
and(
|
|
563
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
564
|
+
inArray(schema.documents.parentId, documentIds),
|
|
565
|
+
or(
|
|
566
|
+
isNull(schema.documents.trashRootId),
|
|
567
|
+
ne(schema.documents.trashRootId, id),
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
return deleteCollectedDocuments(
|
|
573
|
+
db,
|
|
574
|
+
documentIds,
|
|
575
|
+
ownedDatabaseIds,
|
|
576
|
+
ownerEmail,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
356
580
|
export default defineAction({
|
|
357
|
-
description:
|
|
581
|
+
description:
|
|
582
|
+
"Move a document and all its children to Trash. Use permanently-delete-document to destroy an item already in Trash.",
|
|
358
583
|
schema: z.object({
|
|
359
584
|
id: z.string().optional().describe("Document ID (required)"),
|
|
360
585
|
databaseDocumentId: z
|
|
@@ -408,10 +633,12 @@ export default defineAction({
|
|
|
408
633
|
if (systemDatabase?.systemRole) {
|
|
409
634
|
throw new Error("System Content database documents cannot be deleted");
|
|
410
635
|
}
|
|
411
|
-
const deleted = await
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
636
|
+
const deleted = await db.transaction((tx) =>
|
|
637
|
+
trashDocumentSubtree(
|
|
638
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
639
|
+
id,
|
|
640
|
+
existing.ownerEmail as string,
|
|
641
|
+
),
|
|
415
642
|
);
|
|
416
643
|
|
|
417
644
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
@@ -70,7 +70,10 @@ export default defineAction({
|
|
|
70
70
|
statusCode: 404,
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
-
if (
|
|
73
|
+
if (
|
|
74
|
+
access.resource.trashedAt ||
|
|
75
|
+
(await isSoftDeletedDatabaseDocument(args.id))
|
|
76
|
+
) {
|
|
74
77
|
throw Object.assign(new Error(`Document "${args.id}" not found`), {
|
|
75
78
|
statusCode: 404,
|
|
76
79
|
});
|
|
@@ -34,6 +34,7 @@ export default defineAction({
|
|
|
34
34
|
deletedAt: schema.contentDatabases.deletedAt,
|
|
35
35
|
documentTitle: schema.documents.title,
|
|
36
36
|
documentParentId: schema.documents.parentId,
|
|
37
|
+
documentTrashRootId: schema.documents.trashRootId,
|
|
37
38
|
})
|
|
38
39
|
.from(schema.contentDatabases)
|
|
39
40
|
.innerJoin(
|
|
@@ -47,6 +48,10 @@ export default defineAction({
|
|
|
47
48
|
.where(
|
|
48
49
|
and(
|
|
49
50
|
isNotNull(schema.contentDatabases.deletedAt),
|
|
51
|
+
or(
|
|
52
|
+
isNull(schema.documents.trashRootId),
|
|
53
|
+
eq(schema.documents.trashRootId, schema.documents.id),
|
|
54
|
+
),
|
|
50
55
|
or(
|
|
51
56
|
and(
|
|
52
57
|
isBlockOwned,
|
|
@@ -82,8 +87,9 @@ export default defineAction({
|
|
|
82
87
|
ownerDocumentId: row.ownerDocumentId,
|
|
83
88
|
deletedAt: row.deletedAt!,
|
|
84
89
|
canPermanentlyDelete:
|
|
85
|
-
row.
|
|
86
|
-
row.
|
|
90
|
+
row.documentTrashRootId === row.documentId &&
|
|
91
|
+
(row.ownerDocumentId === null ||
|
|
92
|
+
row.documentParentId !== row.ownerDocumentId),
|
|
87
93
|
})),
|
|
88
94
|
};
|
|
89
95
|
},
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, desc, eq, isNotNull, isNull } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import type { ListTrashedDocumentsResponse } from "../shared/api.js";
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"List trashed page roots the current user can manage. Titles are returned only after document access filtering.",
|
|
12
|
+
schema: z.object({}),
|
|
13
|
+
http: { method: "GET" },
|
|
14
|
+
readOnly: true,
|
|
15
|
+
run: async (): Promise<ListTrashedDocumentsResponse> => {
|
|
16
|
+
const rows = await getDb()
|
|
17
|
+
.select({
|
|
18
|
+
documentId: schema.documents.id,
|
|
19
|
+
title: schema.documents.title,
|
|
20
|
+
trashedAt: schema.documents.trashedAt,
|
|
21
|
+
})
|
|
22
|
+
.from(schema.documents)
|
|
23
|
+
.leftJoin(
|
|
24
|
+
schema.contentDatabases,
|
|
25
|
+
eq(schema.contentDatabases.documentId, schema.documents.id),
|
|
26
|
+
)
|
|
27
|
+
.where(
|
|
28
|
+
and(
|
|
29
|
+
isNotNull(schema.documents.trashedAt),
|
|
30
|
+
eq(schema.documents.trashRootId, schema.documents.id),
|
|
31
|
+
isNull(schema.contentDatabases.id),
|
|
32
|
+
accessFilter(
|
|
33
|
+
schema.documents,
|
|
34
|
+
schema.documentShares,
|
|
35
|
+
undefined,
|
|
36
|
+
"admin",
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
)
|
|
40
|
+
.orderBy(desc(schema.documents.trashedAt));
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
documents: rows.map((row) => ({
|
|
44
|
+
documentId: row.documentId,
|
|
45
|
+
title: row.title.trim() || "Untitled",
|
|
46
|
+
trashedAt: row.trashedAt!,
|
|
47
|
+
})),
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { writeAppState } from "@agent-native/core/application-state";
|
|
3
|
+
import { assertAccess } from "@agent-native/core/sharing";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb } from "../server/db/index.js";
|
|
7
|
+
import { deleteTrashedDocumentSubtree } from "./delete-document.js";
|
|
8
|
+
|
|
9
|
+
export default defineAction({
|
|
10
|
+
description:
|
|
11
|
+
"Permanently delete a document subtree that is already in Trash. This cannot be undone.",
|
|
12
|
+
schema: z.object({
|
|
13
|
+
id: z.string().describe("Trashed root document ID"),
|
|
14
|
+
}),
|
|
15
|
+
run: async ({ id }) => {
|
|
16
|
+
const access = await assertAccess("document", id, "admin");
|
|
17
|
+
const db = getDb();
|
|
18
|
+
const deleted = await db.transaction((tx) =>
|
|
19
|
+
deleteTrashedDocumentSubtree(
|
|
20
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
21
|
+
id,
|
|
22
|
+
access.resource.ownerEmail as string,
|
|
23
|
+
),
|
|
24
|
+
);
|
|
25
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
26
|
+
return { success: true, deleted: deleted.length };
|
|
27
|
+
},
|
|
28
|
+
});
|