@agent-native/core 0.134.0 → 0.134.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.
Files changed (57) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  6. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  7. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  8. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  9. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  10. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  11. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  14. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  15. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  16. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  17. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  18. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  19. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  20. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  22. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  23. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  24. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  25. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  26. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  27. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  28. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  29. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  30. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  31. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  32. package/corpus/templates/content/app/i18n-data.ts +130 -0
  33. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  34. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  35. package/corpus/templates/content/parity/matrix.md +1 -1
  36. package/corpus/templates/content/parity/matrix.ts +4 -5
  37. package/corpus/templates/content/shared/api.ts +4 -0
  38. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  39. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  41. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  42. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  43. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  44. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  45. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  46. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  47. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  48. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  49. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  50. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  51. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  52. package/corpus/templates/design/package.json +4 -0
  53. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  54. package/dist/notifications/routes.d.ts +3 -3
  55. package/docs/content/template-content-developers.mdx +1 -1
  56. package/package.json +1 -1
  57. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -28,6 +28,7 @@ import {
28
28
  builderCmsQualifiedId,
29
29
  type BuilderCmsSourceEntry,
30
30
  } from "./_builder-cms-source-adapter.js";
31
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
31
32
  import {
32
33
  resolveDatabaseForSourceMutation,
33
34
  serializeSourceField,
@@ -460,6 +461,11 @@ export default defineAction({
460
461
  documentId: string;
461
462
  sourceValuesJson: string | null;
462
463
  }> = [];
464
+ const sourceRowUpdates: Array<{
465
+ id: string;
466
+ previousSourceValuesJson: string;
467
+ sourceValuesJson: string;
468
+ }> = [];
463
469
  if (builderEntries) {
464
470
  const currentSourceRows = await tx
465
471
  .select()
@@ -487,27 +493,11 @@ export default defineAction({
487
493
  ) {
488
494
  continue;
489
495
  }
490
- const [updatedRow] = await tx
491
- .update(schema.contentDatabaseSourceRows)
492
- .set({
493
- sourceValuesJson: mergedRow.sourceValuesJson,
494
- updatedAt: now,
495
- })
496
- .where(
497
- and(
498
- eq(schema.contentDatabaseSourceRows.id, currentRow.id),
499
- eq(
500
- schema.contentDatabaseSourceRows.sourceValuesJson,
501
- currentRow.sourceValuesJson,
502
- ),
503
- ),
504
- )
505
- .returning({ id: schema.contentDatabaseSourceRows.id });
506
- if (!updatedRow) {
507
- throw new Error(
508
- "The Builder source changed while adding this property. No property was created; try again.",
509
- );
510
- }
496
+ sourceRowUpdates.push({
497
+ id: currentRow.id,
498
+ previousSourceValuesJson: currentRow.sourceValuesJson,
499
+ sourceValuesJson: mergedRow.sourceValuesJson,
500
+ });
511
501
  }
512
502
  sourceRows = merged.rows;
513
503
  } else if (!isSecondary) {
@@ -545,6 +535,34 @@ export default defineAction({
545
535
  );
546
536
  }
547
537
 
538
+ await lockDatabaseMemberships(
539
+ tx,
540
+ sourceRows.map((row) => row.databaseItemId),
541
+ );
542
+ for (const update of sourceRowUpdates) {
543
+ const [updatedRow] = await tx
544
+ .update(schema.contentDatabaseSourceRows)
545
+ .set({
546
+ sourceValuesJson: update.sourceValuesJson,
547
+ updatedAt: now,
548
+ })
549
+ .where(
550
+ and(
551
+ eq(schema.contentDatabaseSourceRows.id, update.id),
552
+ eq(
553
+ schema.contentDatabaseSourceRows.sourceValuesJson,
554
+ update.previousSourceValuesJson,
555
+ ),
556
+ ),
557
+ )
558
+ .returning({ id: schema.contentDatabaseSourceRows.id });
559
+ if (!updatedRow) {
560
+ throw new Error(
561
+ "The Builder source changed while adding this property. No property was created; try again.",
562
+ );
563
+ }
564
+ }
565
+
548
566
  const builderMetadata = builderMetadataForSourceField({
549
567
  sourceFieldKey: currentField.sourceFieldKey,
550
568
  sourceMetadataJson: currentSource.metadataJson,
@@ -27,6 +27,7 @@ import {
27
27
  importBuilderCmsEntriesAsDatabaseItems,
28
28
  insertSecondarySource,
29
29
  mapBuilderCmsEntriesToLocalItems,
30
+ replaceMockSourceRows,
30
31
  replaceSourceMetadata,
31
32
  resolveDatabaseForSourceMutation,
32
33
  seedMockSourceFields,
@@ -511,6 +512,7 @@ export default defineAction({
511
512
  if (additionalRead.state === "live") {
512
513
  const importResult = await importBuilderCmsEntriesAsDatabaseItems({
513
514
  database,
515
+ sourceId: additionalSourceId,
514
516
  entries: additionalEntries,
515
517
  now,
516
518
  sourceTable,
@@ -634,6 +636,7 @@ export default defineAction({
634
636
  if (builderRead?.state === "live") {
635
637
  const importResult = await importBuilderCmsEntriesAsDatabaseItems({
636
638
  database,
639
+ sourceId,
637
640
  entries: builderEntries,
638
641
  now,
639
642
  sourceTable,
@@ -682,7 +685,7 @@ export default defineAction({
682
685
  builderSampleEntries: builderEntries,
683
686
  now,
684
687
  });
685
- await seedMockSourceRows({
688
+ await replaceMockSourceRows({
686
689
  sourceId,
687
690
  ownerEmail: database.ownerEmail,
688
691
  sourceType,
@@ -23,9 +23,9 @@ import {
23
23
  importBuilderCmsEntriesAsDatabaseItems,
24
24
  mapBuilderCmsEntriesToLocalItems,
25
25
  mutateContentDatabaseSourceMetadata,
26
+ replaceMockSourceRows,
26
27
  resolveDatabaseForSourceMutation,
27
28
  seedMockSourceFields,
28
- seedMockSourceRows,
29
29
  seedSecondarySourceFields,
30
30
  sourceSetupPayload,
31
31
  storeSecondarySourceRows,
@@ -400,9 +400,6 @@ export default defineAction({
400
400
  await db
401
401
  .delete(schema.contentDatabaseSourceFields)
402
402
  .where(eq(schema.contentDatabaseSourceFields.sourceId, source.id));
403
- await db
404
- .delete(schema.contentDatabaseSourceRows)
405
- .where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
406
403
  await clearSourceFederation(source.id, now);
407
404
 
408
405
  let importedEntriesByDocumentId = new Map<
@@ -412,6 +409,7 @@ export default defineAction({
412
409
  if (read.state === "live") {
413
410
  const importResult = await importBuilderCmsEntriesAsDatabaseItems({
414
411
  database,
412
+ sourceId: source.id,
415
413
  entries,
416
414
  now,
417
415
  sourceTable: source.sourceTable,
@@ -443,7 +441,7 @@ export default defineAction({
443
441
  builderSampleEntries: entries,
444
442
  now,
445
443
  });
446
- await seedMockSourceRows({
444
+ await replaceMockSourceRows({
447
445
  sourceId: source.id,
448
446
  ownerEmail: database.ownerEmail,
449
447
  sourceType: "builder-cms",
@@ -10,6 +10,7 @@ import {
10
10
  serializePropertyOptions,
11
11
  type DocumentPropertyType,
12
12
  } from "../shared/properties.js";
13
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
13
14
  import {
14
15
  propertyDefinitionsPositionScope,
15
16
  withPositionLock,
@@ -76,57 +77,69 @@ export default defineAction({
76
77
  await withPositionLock(
77
78
  propertyDefinitionsPositionScope(database.id),
78
79
  async () => {
79
- const [maxPos] = await db
80
- .select({
81
- max: sql<number>`COALESCE(MAX(position), -1)`,
82
- })
83
- .from(schema.documentPropertyDefinitions)
84
- .where(
85
- and(
86
- eq(
87
- schema.documentPropertyDefinitions.ownerEmail,
88
- document.ownerEmail,
89
- ),
90
- eq(schema.documentPropertyDefinitions.databaseId, database.id),
91
- ),
80
+ await db.transaction(async (tx) => {
81
+ const memberships = await tx
82
+ .select({ id: schema.contentDatabaseItems.id })
83
+ .from(schema.contentDatabaseItems)
84
+ .where(eq(schema.contentDatabaseItems.databaseId, database.id));
85
+ await lockDatabaseMemberships(
86
+ tx,
87
+ memberships.map((membership) => membership.id),
92
88
  );
93
89
 
94
- await db.insert(schema.documentPropertyDefinitions).values({
95
- id: newPropertyId,
96
- ownerEmail: definition.ownerEmail,
97
- orgId: definition.orgId,
98
- databaseId: database.id,
99
- name: `${definition.name} copy`,
100
- type: definition.type,
101
- visibility: definition.visibility,
102
- optionsJson,
103
- position: (maxPos?.max ?? -1) + 1,
104
- createdAt: now,
105
- updatedAt: now,
106
- });
107
- },
108
- );
90
+ const [maxPos] = await tx
91
+ .select({
92
+ max: sql<number>`COALESCE(MAX(position), -1)`,
93
+ })
94
+ .from(schema.documentPropertyDefinitions)
95
+ .where(
96
+ and(
97
+ eq(
98
+ schema.documentPropertyDefinitions.ownerEmail,
99
+ document.ownerEmail,
100
+ ),
101
+ eq(schema.documentPropertyDefinitions.databaseId, database.id),
102
+ ),
103
+ );
109
104
 
110
- // Blocks fields don't use document_property_values; a duplicate starts empty.
111
- if (!isBlocks) {
112
- const values = await db
113
- .select()
114
- .from(schema.documentPropertyValues)
115
- .where(eq(schema.documentPropertyValues.propertyId, propertyId));
116
- if (values.length > 0) {
117
- await db.insert(schema.documentPropertyValues).values(
118
- values.map((value) => ({
119
- id: nanoid(),
120
- ownerEmail: value.ownerEmail,
121
- documentId: value.documentId,
122
- propertyId: newPropertyId,
123
- valueJson: value.valueJson,
105
+ await tx.insert(schema.documentPropertyDefinitions).values({
106
+ id: newPropertyId,
107
+ ownerEmail: definition.ownerEmail,
108
+ orgId: definition.orgId,
109
+ databaseId: database.id,
110
+ name: `${definition.name} copy`,
111
+ type: definition.type,
112
+ visibility: definition.visibility,
113
+ optionsJson,
114
+ position: (maxPos?.max ?? -1) + 1,
124
115
  createdAt: now,
125
116
  updatedAt: now,
126
- })),
127
- );
128
- }
129
- }
117
+ });
118
+
119
+ // Blocks fields don't use document_property_values; a duplicate
120
+ // starts empty.
121
+ if (!isBlocks) {
122
+ const values = await tx
123
+ .select()
124
+ .from(schema.documentPropertyValues)
125
+ .where(eq(schema.documentPropertyValues.propertyId, propertyId));
126
+ if (values.length > 0) {
127
+ await tx.insert(schema.documentPropertyValues).values(
128
+ values.map((value) => ({
129
+ id: nanoid(),
130
+ ownerEmail: value.ownerEmail,
131
+ documentId: value.documentId,
132
+ propertyId: newPropertyId,
133
+ valueJson: value.valueJson,
134
+ createdAt: now,
135
+ updatedAt: now,
136
+ })),
137
+ );
138
+ }
139
+ }
140
+ });
141
+ },
142
+ );
130
143
 
131
144
  await writeAppState("refresh-signal", { ts: Date.now() });
132
145
 
@@ -44,6 +44,7 @@ import {
44
44
  executeBuilderCmsWrite,
45
45
  } from "./_builder-cms-write-client.js";
46
46
  import { createBuilderSourceTiming } from "./_builder-source-timings.js";
47
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
47
48
  import {
48
49
  getContentDatabaseSourceSnapshotForWrite,
49
50
  resolveDatabaseForSourceMutation,
@@ -563,80 +564,85 @@ async function reconcileBuilderCmsWrite(args: {
563
564
  }
564
565
 
565
566
  const db = getDb();
566
- const existingRow =
567
- args.changeSet.documentId || args.changeSet.databaseItemId
568
- ? await db
569
- .select()
570
- .from(schema.contentDatabaseSourceRows)
571
- .where(
572
- and(
573
- eq(schema.contentDatabaseSourceRows.sourceId, args.source.id),
574
- args.changeSet.documentId
575
- ? eq(
576
- schema.contentDatabaseSourceRows.documentId,
577
- args.changeSet.documentId,
578
- )
579
- : eq(
580
- schema.contentDatabaseSourceRows.databaseItemId,
581
- args.changeSet.databaseItemId as string,
582
- ),
583
- ),
584
- )
585
- .limit(1)
586
- : [];
567
+ await db.transaction(async (tx) => {
568
+ if (args.changeSet.databaseItemId) {
569
+ await lockDatabaseMemberships(tx, [args.changeSet.databaseItemId]);
570
+ }
571
+ const existingRow =
572
+ args.changeSet.documentId || args.changeSet.databaseItemId
573
+ ? await tx
574
+ .select()
575
+ .from(schema.contentDatabaseSourceRows)
576
+ .where(
577
+ and(
578
+ eq(schema.contentDatabaseSourceRows.sourceId, args.source.id),
579
+ args.changeSet.documentId
580
+ ? eq(
581
+ schema.contentDatabaseSourceRows.documentId,
582
+ args.changeSet.documentId,
583
+ )
584
+ : eq(
585
+ schema.contentDatabaseSourceRows.databaseItemId,
586
+ args.changeSet.databaseItemId as string,
587
+ ),
588
+ ),
589
+ )
590
+ .limit(1)
591
+ : [];
587
592
 
588
- const [row] = existingRow;
589
- const snapshotRow = sourceRowForChangeSet(args.source, args.changeSet);
590
- const sourceValuesJson = builderCmsReconciledSourceValuesJson({
591
- existingSourceValuesJson: row?.sourceValuesJson,
592
- snapshotSourceValues: snapshotRow?.sourceValues,
593
- changeSet: args.changeSet,
594
- plan: args.plan,
595
- });
596
- const patchWithValues = {
597
- ...patch,
598
- sourceValuesJson,
599
- };
600
- if (row) {
601
- await db
602
- .update(schema.contentDatabaseSourceRows)
603
- .set(patchWithValues)
604
- .where(eq(schema.contentDatabaseSourceRows.id, row.id));
605
- } else if (args.changeSet.documentId && args.changeSet.databaseItemId) {
606
- await db.insert(schema.contentDatabaseSourceRows).values({
607
- id: crypto.randomUUID(),
608
- ownerEmail: args.database.ownerEmail,
609
- sourceId: args.source.id,
610
- databaseItemId: args.changeSet.databaseItemId,
611
- documentId: args.changeSet.documentId,
612
- createdAt: args.now,
613
- ...patchWithValues,
593
+ const [row] = existingRow;
594
+ const snapshotRow = sourceRowForChangeSet(args.source, args.changeSet);
595
+ const sourceValuesJson = builderCmsReconciledSourceValuesJson({
596
+ existingSourceValuesJson: row?.sourceValuesJson,
597
+ snapshotSourceValues: snapshotRow?.sourceValues,
598
+ changeSet: args.changeSet,
599
+ plan: args.plan,
614
600
  });
615
- } else {
616
- throw new Error(
617
- "Builder write succeeded, but the local source row was missing.",
618
- );
619
- }
601
+ const patchWithValues = {
602
+ ...patch,
603
+ sourceValuesJson,
604
+ };
605
+ if (row) {
606
+ await tx
607
+ .update(schema.contentDatabaseSourceRows)
608
+ .set(patchWithValues)
609
+ .where(eq(schema.contentDatabaseSourceRows.id, row.id));
610
+ } else if (args.changeSet.documentId && args.changeSet.databaseItemId) {
611
+ await tx.insert(schema.contentDatabaseSourceRows).values({
612
+ id: crypto.randomUUID(),
613
+ ownerEmail: args.database.ownerEmail,
614
+ sourceId: args.source.id,
615
+ databaseItemId: args.changeSet.databaseItemId,
616
+ documentId: args.changeSet.documentId,
617
+ createdAt: args.now,
618
+ ...patchWithValues,
619
+ });
620
+ } else {
621
+ throw new Error(
622
+ "Builder write succeeded, but the local source row was missing.",
623
+ );
624
+ }
620
625
 
621
- await db
622
- .update(schema.contentDatabaseSourceFields)
623
- .set({
624
- freshness: "fresh",
625
- lastSyncedAt: args.now,
626
- updatedAt: args.now,
627
- })
628
- .where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
629
- await db
630
- .update(schema.contentDatabaseSources)
631
- .set({
632
- syncState: "idle",
633
- freshness: "fresh",
634
- lastRefreshedAt: args.now,
635
- lastSourceUpdatedAt: patch.lastSourceUpdatedAt,
636
- lastError: null,
637
- updatedAt: args.now,
638
- })
639
- .where(eq(schema.contentDatabaseSources.id, args.source.id));
626
+ await tx
627
+ .update(schema.contentDatabaseSourceFields)
628
+ .set({
629
+ freshness: "fresh",
630
+ lastSyncedAt: args.now,
631
+ updatedAt: args.now,
632
+ })
633
+ .where(eq(schema.contentDatabaseSourceFields.sourceId, args.source.id));
634
+ await tx
635
+ .update(schema.contentDatabaseSources)
636
+ .set({
637
+ syncState: "idle",
638
+ freshness: "fresh",
639
+ lastRefreshedAt: args.now,
640
+ lastSourceUpdatedAt: patch.lastSourceUpdatedAt,
641
+ lastError: null,
642
+ updatedAt: args.now,
643
+ })
644
+ .where(eq(schema.contentDatabaseSources.id, args.source.id));
645
+ });
640
646
  }
641
647
 
642
648
  export function realExecutionDeps(
@@ -19,6 +19,7 @@ import {
19
19
  } from "../shared/properties.js";
20
20
  import { chunks } from "./_batch-utils.js";
21
21
  import { readBuilderCmsContentEntries } from "./_builder-cms-read-client.js";
22
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
22
23
  import {
23
24
  builderReferenceIdSourceValueKey,
24
25
  resolveDatabaseForSourceMutation,
@@ -352,6 +353,12 @@ export default defineAction({
352
353
  .select()
353
354
  .from(schema.contentDatabaseSourceRows)
354
355
  .where(eq(schema.contentDatabaseSourceRows.sourceId, source.id));
356
+ await lockDatabaseMemberships(
357
+ tx,
358
+ currentSourceRows
359
+ .map((row) => row.databaseItemId)
360
+ .filter((itemId) => itemId.length > 0),
361
+ );
355
362
  const [maxPosition] = await tx
356
363
  .select({ max: sql<number>`COALESCE(MAX(position), -1)` })
357
364
  .from(schema.documentPropertyDefinitions)
@@ -0,0 +1,183 @@
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 { and, eq, inArray } from "drizzle-orm";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { assertNotWorkspaceCatalogDocuments } from "./_content-space-catalog-guards.js";
8
+ import { lockDatabaseMemberships } from "./_database-membership-lock.js";
9
+ import {
10
+ databaseRowBatchSchema,
11
+ renumberDatabaseRows,
12
+ resolveDatabaseRowsForBatch,
13
+ } from "./_database-row-batch.js";
14
+ import { getContentDatabaseResponse } from "./_database-utils.js";
15
+ import {
16
+ databaseItemsPositionScope,
17
+ withPositionLock,
18
+ } from "./_position-utils.js";
19
+
20
+ export default defineAction({
21
+ description:
22
+ "Remove one or more page memberships from a content database in one atomic batch without deleting the pages. Use this once for two or more selected or named rows instead of looping page operations.",
23
+ schema: databaseRowBatchSchema,
24
+ run: async (args) => {
25
+ const db = getDb();
26
+ const { database, rows } = await resolveDatabaseRowsForBatch(args, {
27
+ includeTrashed: true,
28
+ });
29
+
30
+ if (database.systemRole === "favorites") {
31
+ await assertAccess("document", database.documentId, "editor");
32
+ const removedItemIds = rows.map((row) => row.item.id);
33
+ const removedDocumentIds = rows.map((row) => row.document.id);
34
+ const now = new Date().toISOString();
35
+ await withPositionLock(databaseItemsPositionScope(database.id), () =>
36
+ db.transaction(async (tx) => {
37
+ if (removedItemIds.length > 0) {
38
+ await tx
39
+ .delete(schema.contentDatabaseItems)
40
+ .where(inArray(schema.contentDatabaseItems.id, removedItemIds));
41
+ }
42
+ await renumberDatabaseRows(
43
+ tx as unknown as ReturnType<typeof getDb>,
44
+ database,
45
+ now,
46
+ );
47
+ }),
48
+ );
49
+ await writeAppState("refresh-signal", { ts: Date.now() });
50
+ return {
51
+ ...(await getContentDatabaseResponse(database.id, {
52
+ limit: 100,
53
+ offset: 0,
54
+ })),
55
+ removedItemIds,
56
+ removedDocumentIds,
57
+ removedCount: removedItemIds.length,
58
+ };
59
+ }
60
+
61
+ await assertAccess("document", database.documentId, "admin");
62
+ for (const row of rows) {
63
+ await assertAccess("document", row.document.id, "viewer");
64
+ }
65
+
66
+ const removedItemIds = rows.map((row) => row.item.id);
67
+ const removedDocumentIds = rows.map((row) => row.document.id);
68
+ await assertNotWorkspaceCatalogDocuments(
69
+ db,
70
+ removedDocumentIds,
71
+ "removed from a database",
72
+ );
73
+ if (database.systemRole) {
74
+ throw new Error(
75
+ "System database memberships cannot be removed from this surface.",
76
+ );
77
+ }
78
+
79
+ const now = new Date().toISOString();
80
+
81
+ await withPositionLock(databaseItemsPositionScope(database.id), () =>
82
+ db.transaction(async (tx) => {
83
+ if (removedItemIds.length > 0) {
84
+ await lockDatabaseMemberships(tx, removedItemIds);
85
+ const [sourceRow, hydrationRow] = await Promise.all([
86
+ tx
87
+ .select({ id: schema.contentDatabaseSourceRows.id })
88
+ .from(schema.contentDatabaseSourceRows)
89
+ .where(
90
+ inArray(
91
+ schema.contentDatabaseSourceRows.databaseItemId,
92
+ removedItemIds,
93
+ ),
94
+ )
95
+ .limit(1),
96
+ tx
97
+ .select({ id: schema.contentDatabaseBodyHydrationQueue.id })
98
+ .from(schema.contentDatabaseBodyHydrationQueue)
99
+ .where(
100
+ inArray(
101
+ schema.contentDatabaseBodyHydrationQueue.databaseItemId,
102
+ removedItemIds,
103
+ ),
104
+ )
105
+ .limit(1),
106
+ ]);
107
+ if (sourceRow.length > 0 || hydrationRow.length > 0) {
108
+ throw new Error(
109
+ "Source-backed rows cannot be removed from this database.",
110
+ );
111
+ }
112
+ }
113
+ const propertyIds = (
114
+ await tx
115
+ .select({ id: schema.documentPropertyDefinitions.id })
116
+ .from(schema.documentPropertyDefinitions)
117
+ .where(
118
+ eq(schema.documentPropertyDefinitions.databaseId, database.id),
119
+ )
120
+ ).map((property) => property.id);
121
+ if (removedDocumentIds.length > 0 && propertyIds.length > 0) {
122
+ await tx
123
+ .delete(schema.documentPropertyValues)
124
+ .where(
125
+ and(
126
+ inArray(
127
+ schema.documentPropertyValues.documentId,
128
+ removedDocumentIds,
129
+ ),
130
+ inArray(schema.documentPropertyValues.propertyId, propertyIds),
131
+ ),
132
+ );
133
+ await tx
134
+ .delete(schema.documentBlockFieldContents)
135
+ .where(
136
+ and(
137
+ inArray(
138
+ schema.documentBlockFieldContents.documentId,
139
+ removedDocumentIds,
140
+ ),
141
+ inArray(
142
+ schema.documentBlockFieldContents.propertyId,
143
+ propertyIds,
144
+ ),
145
+ ),
146
+ );
147
+ }
148
+ if (removedItemIds.length > 0) {
149
+ await tx
150
+ .delete(schema.contentDatabaseItems)
151
+ .where(inArray(schema.contentDatabaseItems.id, removedItemIds));
152
+ }
153
+ await renumberDatabaseRows(
154
+ tx as unknown as ReturnType<typeof getDb>,
155
+ database,
156
+ now,
157
+ );
158
+
159
+ if (removedItemIds.length > 0) {
160
+ const remaining = await tx
161
+ .select({ id: schema.contentDatabaseItems.id })
162
+ .from(schema.contentDatabaseItems)
163
+ .where(inArray(schema.contentDatabaseItems.id, removedItemIds));
164
+ if (remaining.length > 0) {
165
+ throw new Error("Database memberships were not fully removed.");
166
+ }
167
+ }
168
+ }),
169
+ );
170
+
171
+ await writeAppState("refresh-signal", { ts: Date.now() });
172
+
173
+ return {
174
+ ...(await getContentDatabaseResponse(database.id, {
175
+ limit: 100,
176
+ offset: 0,
177
+ })),
178
+ removedItemIds,
179
+ removedDocumentIds,
180
+ removedCount: removedItemIds.length,
181
+ };
182
+ },
183
+ });