@agent-native/core 0.136.5 → 0.137.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 (174) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/app/components/AgentCompletionSound.tsx +105 -0
  3. package/corpus/templates/analytics/app/components/layout/Layout.tsx +44 -35
  4. package/corpus/templates/analytics/app/i18n-data.ts +38 -0
  5. package/corpus/templates/analytics/app/pages/Settings.tsx +53 -1
  6. package/corpus/templates/analytics/app/pages/settings/settings-search.ts +6 -0
  7. package/corpus/templates/analytics/changelog/2026-08-04-choose-whether-analytics-plays-a-bell-when-the-agent-finishe.md +6 -0
  8. package/corpus/templates/analytics/shared/analytics-user-prefs.ts +2 -0
  9. package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
  10. package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
  11. package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
  12. package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
  13. package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
  14. package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
  15. package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
  16. package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
  17. package/corpus/templates/content/actions/_database-utils.ts +6 -0
  18. package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
  19. package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
  20. package/corpus/templates/content/actions/configure-document-property.ts +113 -32
  21. package/corpus/templates/content/actions/delete-content-database.ts +5 -7
  22. package/corpus/templates/content/actions/delete-document-property.ts +109 -62
  23. package/corpus/templates/content/actions/delete-document.ts +178 -36
  24. package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
  25. package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
  26. package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
  27. package/corpus/templates/content/actions/remove-database-items.ts +13 -0
  28. package/corpus/templates/content/actions/set-document-property.ts +69 -1
  29. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
  30. package/corpus/templates/content/parity/matrix.md +1 -1
  31. package/corpus/templates/content/parity/matrix.ts +1 -0
  32. package/corpus/templates/content/server/db/schema.ts +27 -0
  33. package/corpus/templates/content/server/plugins/db.ts +24 -0
  34. package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
  35. package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
  36. package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
  37. package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
  38. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
  39. package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
  40. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
  41. package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
  42. package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
  43. package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
  44. package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
  45. package/corpus/templates/design/app/i18n-data.ts +8 -0
  46. package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
  47. package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
  48. package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
  49. package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
  50. package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
  51. package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
  52. package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
  53. package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
  54. package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
  55. package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
  56. package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
  57. package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
  58. package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
  59. package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
  60. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
  61. package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
  62. package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
  63. package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
  64. package/corpus/templates/factory/.env.example +17 -0
  65. package/corpus/templates/factory/AGENTS.md +27 -17
  66. package/corpus/templates/factory/README.md +19 -9
  67. package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
  68. package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
  69. package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
  70. package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
  71. package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
  72. package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
  73. package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
  74. package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
  75. package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
  76. package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
  77. package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
  78. package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
  79. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
  80. package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
  81. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
  82. package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
  83. package/corpus/templates/factory/app/routes/factory.tsx +462 -9
  84. package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
  85. package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
  86. package/corpus/templates/factory/netlify.toml +2 -2
  87. package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
  88. package/corpus/templates/factory/server/connectors/slack.ts +133 -5
  89. package/corpus/templates/factory/server/db/schema.ts +6 -0
  90. package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
  91. package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
  92. package/corpus/templates/factory/server/plugins/auth.ts +1 -0
  93. package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
  94. package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
  95. package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
  96. package/corpus/templates/factory/server/triage/contracts.ts +14 -1
  97. package/corpus/templates/factory/server/triage/github-client.ts +404 -0
  98. package/corpus/templates/factory/server/triage/metadata.ts +44 -0
  99. package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
  100. package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
  101. package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
  102. package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
  103. package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
  104. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
  105. package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
  106. package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
  107. package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
  108. package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
  109. package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
  110. package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
  111. package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
  112. package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
  113. package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
  114. package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
  115. package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
  116. package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
  117. package/corpus/templates/plan/shared/plan-content.ts +17 -2
  118. package/corpus/templates/slides/app/components/editor/NewDeckReferenceStep.tsx +321 -0
  119. package/corpus/templates/slides/app/components/editor/PromptDialog.tsx +32 -11
  120. package/corpus/templates/slides/app/lib/recent-references.ts +82 -0
  121. package/corpus/templates/slides/app/pages/DeckEditor.tsx +3 -0
  122. package/corpus/templates/slides/app/pages/Index.tsx +236 -181
  123. package/dist/cli/skills-content/canvas.d.ts +3 -3
  124. package/dist/cli/skills-content/canvas.js +16 -5
  125. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  126. package/dist/cli/skills-content/visual-plan-skill.js +27 -0
  127. package/dist/cli/skills-content/wireframe.d.ts +2 -2
  128. package/dist/cli/skills-content/wireframe.js +7 -0
  129. package/dist/client/AgentPanel.js +23 -8
  130. package/dist/client/DefaultSpinner.d.ts +0 -12
  131. package/dist/client/DefaultSpinner.js +22 -2
  132. package/dist/client/api-path.d.ts +6 -0
  133. package/dist/client/api-path.js +55 -0
  134. package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
  135. package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
  136. package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
  137. package/dist/client/onboarding/first-run-enabled.js +9 -0
  138. package/dist/client/onboarding/index.d.ts +2 -1
  139. package/dist/client/onboarding/index.js +1 -0
  140. package/dist/client/onboarding/use-onboarding.d.ts +6 -1
  141. package/dist/client/onboarding/use-onboarding.js +35 -1
  142. package/dist/client/route-state.js +10 -0
  143. package/dist/collab/awareness.d.ts +2 -2
  144. package/dist/collab/routes.d.ts +1 -1
  145. package/dist/connections/catalog.d.ts +4 -4
  146. package/dist/connections/catalog.js +3 -3
  147. package/dist/db/index.d.ts +1 -1
  148. package/dist/db/index.js +1 -1
  149. package/dist/deploy/build.js +2 -1
  150. package/dist/notifications/routes.d.ts +3 -3
  151. package/dist/observability/routes.d.ts +1 -1
  152. package/dist/onboarding/app-profile.d.ts +4 -0
  153. package/dist/onboarding/app-profile.js +378 -0
  154. package/dist/onboarding/index.d.ts +2 -1
  155. package/dist/onboarding/index.js +1 -0
  156. package/dist/onboarding/plugin.d.ts +2 -0
  157. package/dist/onboarding/plugin.js +21 -1
  158. package/dist/onboarding/types.d.ts +19 -0
  159. package/dist/progress/routes.d.ts +1 -1
  160. package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
  161. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  162. package/dist/secrets/routes.d.ts +3 -3
  163. package/dist/server/auth.js +21 -1
  164. package/dist/server/better-auth-instance.d.ts +3 -2
  165. package/dist/server/better-auth-instance.js +9 -7
  166. package/dist/server/email-markdown.js +3 -8
  167. package/dist/shared/first-run-onboarding.d.ts +3 -0
  168. package/dist/shared/first-run-onboarding.js +3 -0
  169. package/dist/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
  170. package/dist/triggers/index.d.ts +3 -0
  171. package/dist/triggers/index.js +5 -0
  172. package/docs/content/cross-app-sso.mdx +26 -0
  173. package/package.json +1 -1
  174. package/src/templates/workspace-core/.agents/skills/address-feedback/SKILL.md +25 -9
@@ -1,6 +1,6 @@
1
1
  import { defineAction } from "@agent-native/core";
2
2
  import { assertAccess } from "@agent-native/core/sharing";
3
- import { and, asc, eq, inArray, ne } from "drizzle-orm";
3
+ import { and, asc, eq, inArray, isNull, ne, sql } from "drizzle-orm";
4
4
  import { z } from "zod";
5
5
 
6
6
  import { getDb, schema } from "../server/db/index.js";
@@ -157,41 +157,77 @@ async function clearSourceFederation(sourceId: string, now: string) {
157
157
  });
158
158
  }
159
159
 
160
- async function removeRowsOwnedOnlyBySource(args: {
160
+ export async function removeRowsOwnedOnlyBySource(args: {
161
161
  databaseId: string;
162
162
  sourceId: string;
163
163
  }) {
164
164
  const db = getDb();
165
- const [targetRows, otherRows] = await Promise.all([
166
- db
167
- .select()
168
- .from(schema.contentDatabaseSourceRows)
169
- .where(eq(schema.contentDatabaseSourceRows.sourceId, args.sourceId)),
170
- db
165
+ await db.transaction(async (tx) => {
166
+ const [lockedDatabase] = await tx
167
+ .update(schema.contentDatabases)
168
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
169
+ .where(
170
+ and(
171
+ eq(schema.contentDatabases.id, args.databaseId),
172
+ isNull(schema.contentDatabases.deletedAt),
173
+ ),
174
+ )
175
+ .returning({ id: schema.contentDatabases.id });
176
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
177
+
178
+ const targetRows = await tx
171
179
  .select()
172
180
  .from(schema.contentDatabaseSourceRows)
173
- .where(ne(schema.contentDatabaseSourceRows.sourceId, args.sourceId)),
174
- ]);
175
- const otherDocumentIds = new Set(
176
- otherRows.map((row) => row.documentId).filter(Boolean),
177
- );
178
- const itemIds = targetRows
179
- .filter(
180
- (row) =>
181
- row.databaseItemId &&
182
- row.documentId &&
183
- !otherDocumentIds.has(row.documentId),
184
- )
185
- .map((row) => row.databaseItemId);
186
- if (itemIds.length === 0) return;
187
- await db
188
- .delete(schema.contentDatabaseItems)
189
- .where(
190
- and(
191
- eq(schema.contentDatabaseItems.databaseId, args.databaseId),
192
- inArray(schema.contentDatabaseItems.id, itemIds),
193
- ),
181
+ .where(eq(schema.contentDatabaseSourceRows.sourceId, args.sourceId));
182
+ const siblingSources = await tx
183
+ .select({ id: schema.contentDatabaseSources.id })
184
+ .from(schema.contentDatabaseSources)
185
+ .where(
186
+ and(
187
+ eq(schema.contentDatabaseSources.databaseId, args.databaseId),
188
+ ne(schema.contentDatabaseSources.id, args.sourceId),
189
+ ),
190
+ );
191
+ const otherRows = siblingSources.length
192
+ ? await tx
193
+ .select()
194
+ .from(schema.contentDatabaseSourceRows)
195
+ .where(
196
+ inArray(
197
+ schema.contentDatabaseSourceRows.sourceId,
198
+ siblingSources.map((source) => source.id),
199
+ ),
200
+ )
201
+ : [];
202
+ const otherDocumentIds = new Set(
203
+ otherRows.map((row) => row.documentId).filter(Boolean),
194
204
  );
205
+ const itemIds = targetRows
206
+ .filter(
207
+ (row) =>
208
+ row.databaseItemId &&
209
+ row.documentId &&
210
+ !otherDocumentIds.has(row.documentId),
211
+ )
212
+ .map((row) => row.databaseItemId as string);
213
+ if (itemIds.length === 0) return;
214
+ await tx
215
+ .delete(schema.contentDatabaseItemKeyClaims)
216
+ .where(
217
+ and(
218
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, args.databaseId),
219
+ inArray(schema.contentDatabaseItemKeyClaims.itemId, itemIds),
220
+ ),
221
+ );
222
+ await tx
223
+ .delete(schema.contentDatabaseItems)
224
+ .where(
225
+ and(
226
+ eq(schema.contentDatabaseItems.databaseId, args.databaseId),
227
+ inArray(schema.contentDatabaseItems.id, itemIds),
228
+ ),
229
+ );
230
+ });
195
231
  }
196
232
 
197
233
  export async function readBuilderCmsEntriesForRoleChange(
@@ -1,7 +1,7 @@
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, sql } from "drizzle-orm";
4
+ import { and, eq, isNull, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
@@ -114,12 +114,13 @@ export default defineAction({
114
114
  }
115
115
 
116
116
  if (args.id) {
117
+ const propertyId = args.id;
117
118
  const [existing] = await db
118
119
  .select()
119
120
  .from(schema.documentPropertyDefinitions)
120
121
  .where(
121
122
  and(
122
- eq(schema.documentPropertyDefinitions.id, args.id),
123
+ eq(schema.documentPropertyDefinitions.id, propertyId),
123
124
  eq(
124
125
  schema.documentPropertyDefinitions.ownerEmail,
125
126
  document.ownerEmail,
@@ -127,7 +128,7 @@ export default defineAction({
127
128
  eq(schema.documentPropertyDefinitions.databaseId, database.id),
128
129
  ),
129
130
  );
130
- if (!existing) throw new Error(`Property "${args.id}" not found`);
131
+ if (!existing) throw new Error(`Property "${propertyId}" not found`);
131
132
  if (existing.systemRole) {
132
133
  throw new Error("System properties cannot be changed.");
133
134
  }
@@ -158,42 +159,122 @@ export default defineAction({
158
159
  optionsJson = serializePropertyOptions({ blocks: { primary: true } });
159
160
  }
160
161
 
161
- if (existing.type !== type) {
162
- await db
163
- .delete(schema.documentPropertyValues)
162
+ await db.transaction(async (tx) => {
163
+ const [lockedDatabase] = await tx
164
+ .update(schema.contentDatabases)
165
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
164
166
  .where(
165
167
  and(
166
- eq(schema.documentPropertyValues.propertyId, args.id),
167
- eq(schema.documentPropertyValues.ownerEmail, document.ownerEmail),
168
+ eq(schema.contentDatabases.id, database.id),
169
+ eq(schema.contentDatabases.documentId, database.documentId),
170
+ eq(schema.contentDatabases.ownerEmail, document.ownerEmail),
171
+ isNull(schema.contentDatabases.deletedAt),
168
172
  ),
169
- );
170
- // Switching a Blocks field to another type drops its independent content.
173
+ )
174
+ .returning({ id: schema.contentDatabases.id });
175
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
176
+ const [lockedExisting] = await tx
177
+ .update(schema.documentPropertyDefinitions)
178
+ .set({
179
+ updatedAt: sql`${schema.documentPropertyDefinitions.updatedAt}`,
180
+ })
181
+ .where(
182
+ and(
183
+ eq(schema.documentPropertyDefinitions.id, propertyId),
184
+ eq(
185
+ schema.documentPropertyDefinitions.ownerEmail,
186
+ document.ownerEmail,
187
+ ),
188
+ eq(schema.documentPropertyDefinitions.databaseId, database.id),
189
+ ),
190
+ )
191
+ .returning();
192
+ if (!lockedExisting)
193
+ throw new Error(`Property "${propertyId}" not found`);
194
+ if (lockedExisting.systemRole)
195
+ throw new Error("System properties cannot be changed.");
171
196
  if (
172
- isBlocksPropertyType(existing.type as DocumentPropertyType) &&
173
- !isBlocksPropertyType(type)
197
+ isComputedPropertyType(lockedExisting.type as DocumentPropertyType) &&
198
+ lockedExisting.type !== type
174
199
  ) {
175
- await db
176
- .delete(schema.documentBlockFieldContents)
177
- .where(eq(schema.documentBlockFieldContents.propertyId, args.id));
200
+ throw new Error("Computed property types cannot be changed.");
201
+ }
202
+ const lockedIsPrimaryBlocks =
203
+ isBlocksPropertyType(lockedExisting.type as DocumentPropertyType) &&
204
+ isPrimaryBlocksField(
205
+ parsePropertyOptions(lockedExisting.optionsJson),
206
+ );
207
+ if (lockedIsPrimaryBlocks && lockedExisting.type !== type) {
208
+ throw new Error(
209
+ "The primary Content (Blocks) field cannot change type. Delete it from the database view to remove the body.",
210
+ );
178
211
  }
179
- }
180
212
 
181
- await db
182
- .update(schema.documentPropertyDefinitions)
183
- .set({
184
- name,
185
- ...(args.description === undefined
186
- ? {}
187
- : { description: args.description.trim() }),
188
- type,
189
- visibility:
190
- args.visibility === undefined
191
- ? normalizePropertyVisibility(existing.visibility)
192
- : normalizePropertyVisibility(args.visibility),
193
- optionsJson,
194
- updatedAt: now,
195
- })
196
- .where(eq(schema.documentPropertyDefinitions.id, args.id));
213
+ if (lockedExisting.type !== type) {
214
+ const [mappedSourceField] = await tx
215
+ .select({ id: schema.contentDatabaseSourceFields.id })
216
+ .from(schema.contentDatabaseSourceFields)
217
+ .where(
218
+ eq(schema.contentDatabaseSourceFields.propertyId, propertyId),
219
+ )
220
+ .limit(1);
221
+ if (mappedSourceField) {
222
+ throw new Error(
223
+ "A property bound to a source field must be unbound before changing its type.",
224
+ );
225
+ }
226
+ await tx
227
+ .delete(schema.documentPropertyValues)
228
+ .where(
229
+ and(
230
+ eq(schema.documentPropertyValues.propertyId, propertyId),
231
+ eq(
232
+ schema.documentPropertyValues.ownerEmail,
233
+ document.ownerEmail,
234
+ ),
235
+ ),
236
+ );
237
+ await tx
238
+ .delete(schema.contentDatabaseItemKeyClaims)
239
+ .where(
240
+ and(
241
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
242
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
243
+ ),
244
+ );
245
+ // Switching a Blocks field to another type drops its independent content.
246
+ if (
247
+ isBlocksPropertyType(lockedExisting.type as DocumentPropertyType) &&
248
+ !isBlocksPropertyType(type)
249
+ ) {
250
+ await tx
251
+ .delete(schema.documentBlockFieldContents)
252
+ .where(
253
+ eq(schema.documentBlockFieldContents.propertyId, propertyId),
254
+ );
255
+ }
256
+ }
257
+
258
+ await tx
259
+ .update(schema.documentPropertyDefinitions)
260
+ .set({
261
+ name,
262
+ ...(args.description === undefined
263
+ ? {}
264
+ : { description: args.description.trim() }),
265
+ type,
266
+ visibility:
267
+ args.visibility === undefined
268
+ ? normalizePropertyVisibility(lockedExisting.visibility)
269
+ : normalizePropertyVisibility(args.visibility),
270
+ optionsJson:
271
+ lockedIsPrimaryBlocks && isBlocksPropertyType(type)
272
+ ? serializePropertyOptions({ blocks: { primary: true } })
273
+ : optionsJson,
274
+ updatedAt: now,
275
+ })
276
+ .where(eq(schema.documentPropertyDefinitions.id, propertyId));
277
+ });
197
278
  } else {
198
279
  await withPositionLock(
199
280
  propertyDefinitionsPositionScope(database.id),
@@ -21,13 +21,11 @@ export default defineAction({
21
21
  await assertAccess("document", database.documentId, "admin");
22
22
  const db = getDb();
23
23
  const deletedAt = database.deletedAt ?? new Date().toISOString();
24
- await db.transaction((tx) =>
25
- trashDocumentSubtree(
26
- tx as unknown as ReturnType<typeof getDb>,
27
- database.documentId,
28
- database.ownerEmail,
29
- deletedAt,
30
- ),
24
+ await trashDocumentSubtree(
25
+ db,
26
+ database.documentId,
27
+ database.ownerEmail,
28
+ deletedAt,
31
29
  );
32
30
 
33
31
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -1,7 +1,7 @@
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, isNull, sql } from "drizzle-orm";
5
5
  import { z } from "zod";
6
6
 
7
7
  import { getDb, schema } from "../server/db/index.js";
@@ -65,74 +65,121 @@ export default defineAction({
65
65
  isBlocks &&
66
66
  isPrimaryBlocksField(parsePropertyOptions(definition.optionsJson));
67
67
 
68
- await db
69
- .delete(schema.documentPropertyValues)
70
- .where(eq(schema.documentPropertyValues.propertyId, propertyId));
71
- await db
72
- .delete(schema.documentPropertyDefinitions)
73
- .where(eq(schema.documentPropertyDefinitions.id, propertyId));
68
+ await db.transaction(async (tx) => {
69
+ const [lockedDatabase] = await tx
70
+ .update(schema.contentDatabases)
71
+ .set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
72
+ .where(
73
+ and(
74
+ eq(schema.contentDatabases.id, database.id),
75
+ eq(schema.contentDatabases.documentId, database.documentId),
76
+ eq(schema.contentDatabases.ownerEmail, document.ownerEmail),
77
+ isNull(schema.contentDatabases.deletedAt),
78
+ ),
79
+ )
80
+ .returning({ id: schema.contentDatabases.id });
81
+ if (!lockedDatabase) throw new Error("Database is no longer active.");
82
+ const [lockedDefinition] = await tx
83
+ .update(schema.documentPropertyDefinitions)
84
+ .set({
85
+ updatedAt: sql`${schema.documentPropertyDefinitions.updatedAt}`,
86
+ })
87
+ .where(
88
+ and(
89
+ eq(schema.documentPropertyDefinitions.id, propertyId),
90
+ eq(
91
+ schema.documentPropertyDefinitions.ownerEmail,
92
+ document.ownerEmail,
93
+ ),
94
+ eq(schema.documentPropertyDefinitions.databaseId, database.id),
95
+ ),
96
+ )
97
+ .returning({
98
+ id: schema.documentPropertyDefinitions.id,
99
+ systemRole: schema.documentPropertyDefinitions.systemRole,
100
+ });
101
+ if (!lockedDefinition)
102
+ throw new Error(`Property "${propertyId}" not found`);
103
+ if (lockedDefinition.systemRole)
104
+ throw new Error("System properties cannot be deleted.");
105
+
106
+ await tx
107
+ .delete(schema.documentPropertyValues)
108
+ .where(eq(schema.documentPropertyValues.propertyId, propertyId));
109
+ await tx
110
+ .delete(schema.contentDatabaseItemKeyClaims)
111
+ .where(
112
+ and(
113
+ eq(schema.contentDatabaseItemKeyClaims.databaseId, database.id),
114
+ eq(schema.contentDatabaseItemKeyClaims.propertyId, propertyId),
115
+ ),
116
+ );
117
+ await tx
118
+ .delete(schema.documentPropertyDefinitions)
119
+ .where(eq(schema.documentPropertyDefinitions.id, propertyId));
74
120
 
75
- if (isBlocks) {
76
- // Drop the independent content for this Blocks field across every row.
77
- await db
78
- .delete(schema.documentBlockFieldContents)
79
- .where(eq(schema.documentBlockFieldContents.propertyId, propertyId));
121
+ if (isBlocks) {
122
+ // Drop the independent content for this Blocks field across every row.
123
+ await tx
124
+ .delete(schema.documentBlockFieldContents)
125
+ .where(eq(schema.documentBlockFieldContents.propertyId, propertyId));
80
126
 
81
- // Deleting the primary "Content" field removes the body (documents.content)
82
- // for every object of this type, per the delete warning shown in the UI.
83
- if (isPrimaryBlocks) {
84
- // Record that the primary was intentionally removed: clear the single
85
- // source of truth but LEAVE blocks_seeded = 1, so neither the read path
86
- // nor the startup repair ever recreates it. Deleting the only Blocks
87
- // field is an allowed product action that leaves the row metadata-only
88
- // with ZERO Blocks fields.
89
- await db
90
- .update(schema.contentDatabases)
91
- .set({
92
- primaryBlocksPropertyId: null,
93
- updatedAt: new Date().toISOString(),
94
- })
95
- .where(eq(schema.contentDatabases.id, database.id));
127
+ // Deleting the primary "Content" field removes the body (documents.content)
128
+ // for every object of this type, per the delete warning shown in the UI.
129
+ if (isPrimaryBlocks) {
130
+ // Record that the primary was intentionally removed: clear the single
131
+ // source of truth but LEAVE blocks_seeded = 1, so neither the read path
132
+ // nor the startup repair ever recreates it. Deleting the only Blocks
133
+ // field is an allowed product action that leaves the row metadata-only
134
+ // with ZERO Blocks fields.
135
+ await tx
136
+ .update(schema.contentDatabases)
137
+ .set({
138
+ primaryBlocksPropertyId: null,
139
+ updatedAt: new Date().toISOString(),
140
+ })
141
+ .where(eq(schema.contentDatabases.id, database.id));
96
142
 
97
- const items = await db
98
- .select({ documentId: schema.contentDatabaseItems.documentId })
99
- .from(schema.contentDatabaseItems)
100
- .where(eq(schema.contentDatabaseItems.databaseId, database.id));
101
- const documentIds = items.map((item) => item.documentId);
102
- if (documentIds.length > 0) {
103
- const now = new Date().toISOString();
104
- await db
105
- .update(schema.documents)
106
- .set({ content: "", updatedAt: now })
107
- .where(inArray(schema.documents.id, documentIds));
143
+ const items = await tx
144
+ .select({ documentId: schema.contentDatabaseItems.documentId })
145
+ .from(schema.contentDatabaseItems)
146
+ .where(eq(schema.contentDatabaseItems.databaseId, database.id));
147
+ const documentIds = items.map((item) => item.documentId);
148
+ if (documentIds.length > 0) {
149
+ const now = new Date().toISOString();
150
+ await tx
151
+ .update(schema.documents)
152
+ .set({ content: "", updatedAt: now })
153
+ .where(inArray(schema.documents.id, documentIds));
154
+ }
108
155
  }
109
156
  }
110
- }
111
157
 
112
- // Free any source field that was mapped to this property so it returns to
113
- // the "From source" picker immediately, instead of staying orphaned until
114
- // the next source refresh reconciles it.
115
- const mappedFields = await db
116
- .select({
117
- id: schema.contentDatabaseSourceFields.id,
118
- sourceFieldKey: schema.contentDatabaseSourceFields.sourceFieldKey,
119
- })
120
- .from(schema.contentDatabaseSourceFields)
121
- .where(eq(schema.contentDatabaseSourceFields.propertyId, propertyId));
122
- if (mappedFields.length > 0) {
123
- const now = new Date().toISOString();
124
- for (const mapped of mappedFields) {
125
- await db
126
- .update(schema.contentDatabaseSourceFields)
127
- .set({
128
- propertyId: null,
129
- localFieldKey: mapped.sourceFieldKey,
130
- mappingType: "property",
131
- updatedAt: now,
132
- })
133
- .where(eq(schema.contentDatabaseSourceFields.id, mapped.id));
158
+ // Free any source field that was mapped to this property so it returns to
159
+ // the "From source" picker immediately, instead of staying orphaned until
160
+ // the next source refresh reconciles it.
161
+ const mappedFields = await tx
162
+ .select({
163
+ id: schema.contentDatabaseSourceFields.id,
164
+ sourceFieldKey: schema.contentDatabaseSourceFields.sourceFieldKey,
165
+ })
166
+ .from(schema.contentDatabaseSourceFields)
167
+ .where(eq(schema.contentDatabaseSourceFields.propertyId, propertyId));
168
+ if (mappedFields.length > 0) {
169
+ const now = new Date().toISOString();
170
+ for (const mapped of mappedFields) {
171
+ await tx
172
+ .update(schema.contentDatabaseSourceFields)
173
+ .set({
174
+ propertyId: null,
175
+ localFieldKey: mapped.sourceFieldKey,
176
+ mappingType: "property",
177
+ updatedAt: now,
178
+ })
179
+ .where(eq(schema.contentDatabaseSourceFields.id, mapped.id));
180
+ }
134
181
  }
135
- }
182
+ });
136
183
 
137
184
  await writeAppState("refresh-signal", { ts: Date.now() });
138
185