@agent-native/core 0.136.4 → 0.137.0
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/templates/clips/.agents/skills/recording/SKILL.md +53 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +7 -0
- package/corpus/templates/clips/actions/lib/loom-import-job.ts +24 -4
- package/corpus/templates/clips/app/components/import-menu.tsx +109 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +8 -18
- package/corpus/templates/clips/app/components/library/library-layout.tsx +14 -50
- package/corpus/templates/clips/app/components/library/sort-menu.tsx +20 -6
- package/corpus/templates/clips/app/components/recorder/pre-record-panel.tsx +10 -34
- package/corpus/templates/clips/app/routes/_app.library._index.tsx +0 -2
- package/corpus/templates/clips/changelog/2026-08-03-restart-during-a-recording-now-immediately-starts-a-fresh-ta.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-cleaner-library-controls-with-an-icon-only-sort-button.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-04-upload-and-loom-imports-now-live-under-one-import-menu.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +81 -30
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +436 -158
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +14 -1
- package/corpus/templates/clips/server/plugins/auth.ts +9 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +13 -0
- package/corpus/templates/content/actions/_database-utils.ts +6 -0
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +273 -97
- package/corpus/templates/content/actions/change-content-database-source-role.ts +65 -29
- package/corpus/templates/content/actions/configure-document-property.ts +113 -32
- package/corpus/templates/content/actions/delete-content-database.ts +5 -7
- package/corpus/templates/content/actions/delete-document-property.ts +109 -62
- package/corpus/templates/content/actions/delete-document.ts +178 -36
- package/corpus/templates/content/actions/duplicate-database-item.ts +27 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +31 -1
- package/corpus/templates/content/actions/permanently-delete-document.ts +4 -6
- package/corpus/templates/content/actions/remove-database-items.ts +13 -0
- package/corpus/templates/content/actions/set-document-property.ts +69 -1
- package/corpus/templates/content/actions/upsert-database-item-by-key.ts +814 -0
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +1 -0
- package/corpus/templates/content/server/db/schema.ts +27 -0
- package/corpus/templates/content/server/plugins/db.ts +24 -0
- package/corpus/templates/design/app/components/design/BreakpointBar.tsx +122 -114
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +2 -2
- package/corpus/templates/design/app/components/design/EditPanel.tsx +68 -34
- package/corpus/templates/design/app/components/design/KeyboardShortcutsPanel.tsx +78 -4
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +8 -0
- package/corpus/templates/design/app/components/design/QuestionFlow.tsx +5 -2
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +10 -9
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +31 -69
- package/corpus/templates/design/app/components/design/inspector/design-icons.tsx +80 -106
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/hooks/use-editor-preferences.ts +75 -0
- package/corpus/templates/design/app/i18n-data.ts +8 -0
- package/corpus/templates/design/app/i18n-keyboard-shortcuts.ts +61 -0
- package/corpus/templates/design/app/pages/design-editor/editor-preferences.ts +98 -0
- package/corpus/templates/design/app/pages/design-editor/nudge-intent.ts +491 -0
- package/corpus/templates/design/app/pages/design-editor/paste-placement.ts +133 -0
- package/corpus/templates/design/app/pages/design-editor/png-export-render.ts +8 -0
- package/corpus/templates/design/app/pages/design-editor/selection-state.ts +5 -12
- package/corpus/templates/design/changelog/2026-08-03-arrow-keys-now-reorder-a-layer-inside-an-auto-layout-frame.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-flow-icons-show-the-direction-at-a-glance-instea.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-auto-layout-icons-share-one-outlined-square-style-so-flow-ga.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-has-a-named-preview-section-you-can-expand-replacing-.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-export-preview-shows-the-selected-element-instead-of-the-who.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-inspector-sections-show-an-expand-indicator-so-it-is-clear-w.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-paste-lands-inside-a-selected-frame-instead-of-beside-it.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-add-breakpoint-button-sits-outside-the-breakpoint-select.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-keep-space-below-the-continu.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-agent-s-follow-up-questions-share-the-chat-panel-s-backg.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-export-panel-no-longer-stacks-a-duplicate-export-block-o.md +6 -0
- package/corpus/templates/design/changelog/2026-08-03-the-text-tool-uses-a-t-icon-matching-the-letter-it-inserts.md +6 -0
- package/corpus/templates/factory/.env.example +17 -0
- package/corpus/templates/factory/AGENTS.md +27 -17
- package/corpus/templates/factory/README.md +19 -9
- package/corpus/templates/factory/actions/get-slack-feedback-context.ts +90 -0
- package/corpus/templates/factory/actions/get-triage-config.ts +14 -3
- package/corpus/templates/factory/actions/govern-agent-native-pull-request.ts +386 -0
- package/corpus/templates/factory/actions/list-factory-automations.ts +53 -0
- package/corpus/templates/factory/actions/list-triage-items.ts +7 -2
- package/corpus/templates/factory/actions/poll-github-sources.ts +203 -0
- package/corpus/templates/factory/actions/poll-sentry-errors.ts +161 -0
- package/corpus/templates/factory/actions/poll-slack-channel.ts +4 -2
- package/corpus/templates/factory/actions/run-factory-automation.ts +42 -0
- package/corpus/templates/factory/actions/save-factory-automation.ts +69 -0
- package/corpus/templates/factory/actions/save-triage-config.ts +28 -2
- package/corpus/templates/factory/actions/start-builder-for-item.ts +411 -0
- package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +14 -3
- package/corpus/templates/factory/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/factory/app/hooks/use-navigation-state.ts +7 -0
- package/corpus/templates/factory/app/i18n/en-US.ts +35 -1
- package/corpus/templates/factory/app/routes/factory.tsx +462 -9
- package/corpus/templates/factory/changelog/2026-08-04-added-organization-owned-factory-automations-for-slack-bug-t.md +6 -0
- package/corpus/templates/factory/changelog/2026-08-04-automation-editor-controls-and-model-picker.md +6 -0
- package/corpus/templates/factory/netlify.toml +2 -2
- package/corpus/templates/factory/server/connectors/credentials.ts +6 -5
- package/corpus/templates/factory/server/connectors/slack.ts +133 -5
- package/corpus/templates/factory/server/db/schema.ts +6 -0
- package/corpus/templates/factory/server/lib/require-factory-automation.ts +65 -0
- package/corpus/templates/factory/server/plugins/agent-chat.ts +29 -11
- package/corpus/templates/factory/server/plugins/auth.ts +1 -0
- package/corpus/templates/factory/server/plugins/factory-migrations.ts +12 -0
- package/corpus/templates/factory/server/plugins/factory-scheduler-job.ts +218 -67
- package/corpus/templates/factory/server/triage/builder-executor.ts +4 -0
- package/corpus/templates/factory/server/triage/contracts.ts +14 -1
- package/corpus/templates/factory/server/triage/github-client.ts +404 -0
- package/corpus/templates/factory/server/triage/metadata.ts +44 -0
- package/corpus/templates/factory/server/triage/pr-babysit.ts +5 -5
- package/corpus/templates/factory/server/triage/pr-policy.ts +144 -0
- package/corpus/templates/factory/server/triage/sentry-client.ts +180 -0
- package/corpus/templates/factory/server/triage/slack-client.ts +43 -0
- package/corpus/templates/factory/server/triage/slack-poller.ts +9 -2
- package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +27 -0
- package/corpus/templates/plan/.agents/skills/visual-plan/references/canvas.md +14 -4
- package/corpus/templates/plan/.agents/skills/visual-plan/references/wireframe.md +7 -0
- package/corpus/templates/plan/.agents/skills/visual-recap/references/wireframe.md +7 -0
- package/corpus/templates/plan/actions/create-plan-design.ts +8 -6
- package/corpus/templates/plan/actions/create-prototype-plan.ts +8 -6
- package/corpus/templates/plan/actions/create-ui-plan.ts +7 -7
- package/corpus/templates/plan/actions/get-plan-blocks.ts +7 -1
- package/corpus/templates/plan/actions/validate-plan-input.ts +39 -0
- package/corpus/templates/plan/app/components/plan/CanvasArea.tsx +167 -11
- package/corpus/templates/plan/app/i18n/en-US.ts +1 -0
- package/corpus/templates/plan/changelog/2026-08-04-canvas-zoom-controls-now-explain-the-command-ctrl-plus-scrol.md +6 -0
- package/corpus/templates/plan/changelog/2026-08-04-pinch-to-zoom-is-now-supported-on-touchscreens-in-visual-pla.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +17 -2
- package/dist/cli/skills-content/canvas.d.ts +3 -3
- package/dist/cli/skills-content/canvas.js +16 -5
- package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
- package/dist/cli/skills-content/visual-plan-skill.js +27 -0
- package/dist/cli/skills-content/wireframe.d.ts +2 -2
- package/dist/cli/skills-content/wireframe.js +7 -0
- package/dist/client/AgentPanel.js +23 -8
- package/dist/client/api-path.d.ts +6 -0
- package/dist/client/api-path.js +55 -0
- package/dist/client/onboarding/FirstRunOnboarding.d.ts +3 -0
- package/dist/client/onboarding/FirstRunOnboarding.js +93 -0
- package/dist/client/onboarding/first-run-enabled.d.ts +2 -0
- package/dist/client/onboarding/first-run-enabled.js +9 -0
- package/dist/client/onboarding/index.d.ts +2 -1
- package/dist/client/onboarding/index.js +1 -0
- package/dist/client/onboarding/use-onboarding.d.ts +6 -1
- package/dist/client/onboarding/use-onboarding.js +32 -1
- package/dist/client/route-state.js +10 -0
- package/dist/client/settings/SecretsSection.js +32 -8
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +4 -4
- package/dist/connections/catalog.js +3 -3
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/localization/default-messages.d.ts +5 -0
- package/dist/localization/default-messages.js +5 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/observability/routes.d.ts +3 -3
- package/dist/onboarding/app-profile.d.ts +4 -0
- package/dist/onboarding/app-profile.js +378 -0
- package/dist/onboarding/index.d.ts +2 -1
- package/dist/onboarding/index.js +1 -0
- package/dist/onboarding/plugin.d.ts +2 -0
- package/dist/onboarding/plugin.js +21 -1
- package/dist/onboarding/types.d.ts +19 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +9 -9
- package/dist/provider-api/actions/provider-api.d.ts +12 -12
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +41 -17
- package/dist/server/auth.js +21 -1
- package/dist/server/better-auth-instance.d.ts +3 -2
- package/dist/server/better-auth-instance.js +9 -7
- package/dist/server/email-markdown.js +3 -8
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/first-run-onboarding.d.ts +3 -0
- package/dist/shared/first-run-onboarding.js +3 -0
- package/dist/triggers/index.d.ts +3 -0
- package/dist/triggers/index.js +5 -0
- package/docs/content/cross-app-sso.mdx +26 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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 "${
|
|
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
|
-
|
|
162
|
-
await
|
|
163
|
-
.
|
|
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.
|
|
167
|
-
eq(schema.
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
197
|
+
isComputedPropertyType(lockedExisting.type as DocumentPropertyType) &&
|
|
198
|
+
lockedExisting.type !== type
|
|
174
199
|
) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
|
@@ -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, isNotNull, isNull, ne, or } from "drizzle-orm";
|
|
4
|
+
import { and, eq, inArray, isNotNull, isNull, ne, or, sql } from "drizzle-orm";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
7
7
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -97,6 +97,28 @@ async function selectDatabaseItemDocuments(
|
|
|
97
97
|
return ownedRows.map((row) => ({ documentId: row.id }));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
async function selectMembershipDatabaseIds(
|
|
101
|
+
db: ReturnType<typeof getDb>,
|
|
102
|
+
documentIds: string[],
|
|
103
|
+
ownerEmail: string,
|
|
104
|
+
) {
|
|
105
|
+
const rows: Array<{ databaseId: string }> = [];
|
|
106
|
+
for (const batch of chunks(documentIds, DELETE_BATCH_SIZE)) {
|
|
107
|
+
rows.push(
|
|
108
|
+
...(await db
|
|
109
|
+
.select({ databaseId: schema.contentDatabaseItems.databaseId })
|
|
110
|
+
.from(schema.contentDatabaseItems)
|
|
111
|
+
.where(
|
|
112
|
+
and(
|
|
113
|
+
inArray(schema.contentDatabaseItems.documentId, batch),
|
|
114
|
+
eq(schema.contentDatabaseItems.ownerEmail, ownerEmail),
|
|
115
|
+
),
|
|
116
|
+
)),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return [...new Set(rows.map((row) => row.databaseId))];
|
|
120
|
+
}
|
|
121
|
+
|
|
100
122
|
async function collectDocumentSubtreeForDelete(
|
|
101
123
|
db: ReturnType<typeof getDb>,
|
|
102
124
|
rootId: string,
|
|
@@ -150,22 +172,83 @@ async function collectDocumentSubtreeForDelete(
|
|
|
150
172
|
frontier = [...next];
|
|
151
173
|
}
|
|
152
174
|
|
|
175
|
+
const collectedDocumentIds = [...documentIds];
|
|
176
|
+
const collectedOwnedDatabaseIds = [...ownedDatabaseIds];
|
|
153
177
|
return {
|
|
154
|
-
documentIds:
|
|
155
|
-
ownedDatabaseIds:
|
|
178
|
+
documentIds: collectedDocumentIds,
|
|
179
|
+
ownedDatabaseIds: collectedOwnedDatabaseIds,
|
|
180
|
+
lockDatabaseIds: [
|
|
181
|
+
...new Set([
|
|
182
|
+
...collectedOwnedDatabaseIds,
|
|
183
|
+
...(await selectMembershipDatabaseIds(
|
|
184
|
+
db,
|
|
185
|
+
collectedDocumentIds,
|
|
186
|
+
ownerEmail,
|
|
187
|
+
)),
|
|
188
|
+
]),
|
|
189
|
+
],
|
|
156
190
|
};
|
|
157
191
|
}
|
|
158
192
|
|
|
193
|
+
async function lockDatabasesAndRecollect<
|
|
194
|
+
T extends { lockDatabaseIds: string[] },
|
|
195
|
+
>(
|
|
196
|
+
db: ReturnType<typeof getDb>,
|
|
197
|
+
ownerEmail: string,
|
|
198
|
+
collect: () => Promise<T>,
|
|
199
|
+
): Promise<T> {
|
|
200
|
+
const lockedDatabaseIds = new Set<string>();
|
|
201
|
+
let collected = await collect();
|
|
202
|
+
while (true) {
|
|
203
|
+
const unlockedDatabaseIds = collected.lockDatabaseIds.filter(
|
|
204
|
+
(databaseId) => !lockedDatabaseIds.has(databaseId),
|
|
205
|
+
);
|
|
206
|
+
if (unlockedDatabaseIds.length === 0) return collected;
|
|
207
|
+
for (const batch of chunks(unlockedDatabaseIds, DELETE_BATCH_SIZE)) {
|
|
208
|
+
await db
|
|
209
|
+
.update(schema.contentDatabases)
|
|
210
|
+
.set({ updatedAt: sql`${schema.contentDatabases.updatedAt}` })
|
|
211
|
+
.where(
|
|
212
|
+
and(
|
|
213
|
+
inArray(schema.contentDatabases.id, batch),
|
|
214
|
+
eq(schema.contentDatabases.ownerEmail, ownerEmail),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
for (const databaseId of batch) lockedDatabaseIds.add(databaseId);
|
|
218
|
+
}
|
|
219
|
+
collected = await collect();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
159
223
|
export async function trashDocumentSubtree(
|
|
160
224
|
db: ReturnType<typeof getDb>,
|
|
161
225
|
id: string,
|
|
162
226
|
ownerEmail: string,
|
|
163
227
|
trashedAt = new Date().toISOString(),
|
|
164
228
|
): Promise<string[]> {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
return db.transaction((tx) =>
|
|
230
|
+
trashDocumentSubtreeInTransaction(
|
|
231
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
232
|
+
id,
|
|
233
|
+
ownerEmail,
|
|
234
|
+
trashedAt,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function trashDocumentSubtreeInTransaction(
|
|
240
|
+
db: ReturnType<typeof getDb>,
|
|
241
|
+
id: string,
|
|
242
|
+
ownerEmail: string,
|
|
243
|
+
trashedAt: string,
|
|
244
|
+
): Promise<string[]> {
|
|
245
|
+
// Stable-key upserts lock their canonical database row before creating or
|
|
246
|
+
// updating children. Acquire the same locks, then collect again: an upsert
|
|
247
|
+
// that won the lock before this transaction may have added a child after the
|
|
248
|
+
// first traversal, while one that arrives later must wait until the database
|
|
249
|
+
// has been marked deleted and will fail closed.
|
|
250
|
+
const { documentIds } = await lockDatabasesAndRecollect(db, ownerEmail, () =>
|
|
251
|
+
collectDocumentSubtreeForDelete(db, id, ownerEmail),
|
|
169
252
|
);
|
|
170
253
|
await assertNotWorkspaceCatalogDocuments(db, documentIds, "deleted");
|
|
171
254
|
|
|
@@ -316,14 +399,20 @@ export async function deleteDocumentRecursive(
|
|
|
316
399
|
id: string,
|
|
317
400
|
ownerEmail: string,
|
|
318
401
|
): Promise<string[]> {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
402
|
+
return db.transaction(async (tx) => {
|
|
403
|
+
const scopedDb = tx as unknown as ReturnType<typeof getDb>;
|
|
404
|
+
const { documentIds, ownedDatabaseIds } = await lockDatabasesAndRecollect(
|
|
405
|
+
scopedDb,
|
|
406
|
+
ownerEmail,
|
|
407
|
+
() => collectDocumentSubtreeForDelete(scopedDb, id, ownerEmail),
|
|
408
|
+
);
|
|
409
|
+
return deleteCollectedDocuments(
|
|
410
|
+
scopedDb,
|
|
411
|
+
documentIds,
|
|
412
|
+
ownedDatabaseIds,
|
|
413
|
+
ownerEmail,
|
|
414
|
+
);
|
|
415
|
+
});
|
|
327
416
|
}
|
|
328
417
|
|
|
329
418
|
async function deleteCollectedDocuments(
|
|
@@ -416,6 +505,27 @@ async function deleteCollectedDocuments(
|
|
|
416
505
|
);
|
|
417
506
|
});
|
|
418
507
|
|
|
508
|
+
await deleteWhereIn(ownedDatabaseIds, async (databaseIdBatch) => {
|
|
509
|
+
await db
|
|
510
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
511
|
+
.where(
|
|
512
|
+
inArray(
|
|
513
|
+
schema.contentDatabaseItemKeyClaims.databaseId,
|
|
514
|
+
databaseIdBatch,
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
});
|
|
518
|
+
await deleteWhereIn(documentIds, async (documentIdBatch) => {
|
|
519
|
+
await db
|
|
520
|
+
.delete(schema.contentDatabaseItemKeyClaims)
|
|
521
|
+
.where(
|
|
522
|
+
inArray(
|
|
523
|
+
schema.contentDatabaseItemKeyClaims.documentId,
|
|
524
|
+
documentIdBatch,
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
});
|
|
528
|
+
|
|
419
529
|
await deleteWhereIn(documentIds, async (documentIdBatch) => {
|
|
420
530
|
await db
|
|
421
531
|
.delete(schema.contentDatabaseBodyHydrationQueue)
|
|
@@ -518,6 +628,20 @@ export async function deleteTrashedDocumentSubtree(
|
|
|
518
628
|
db: ReturnType<typeof getDb>,
|
|
519
629
|
id: string,
|
|
520
630
|
ownerEmail: string,
|
|
631
|
+
): Promise<string[]> {
|
|
632
|
+
return db.transaction((tx) =>
|
|
633
|
+
deleteTrashedDocumentSubtreeInTransaction(
|
|
634
|
+
tx as unknown as ReturnType<typeof getDb>,
|
|
635
|
+
id,
|
|
636
|
+
ownerEmail,
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function deleteTrashedDocumentSubtreeInTransaction(
|
|
642
|
+
db: ReturnType<typeof getDb>,
|
|
643
|
+
id: string,
|
|
644
|
+
ownerEmail: string,
|
|
521
645
|
): Promise<string[]> {
|
|
522
646
|
const [root] = await db
|
|
523
647
|
.select({ id: schema.documents.id })
|
|
@@ -537,23 +661,43 @@ export async function deleteTrashedDocumentSubtree(
|
|
|
537
661
|
);
|
|
538
662
|
}
|
|
539
663
|
|
|
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(
|
|
664
|
+
const { documentIds, ownedDatabaseIds } = await lockDatabasesAndRecollect(
|
|
553
665
|
db,
|
|
554
|
-
documentIds,
|
|
555
666
|
ownerEmail,
|
|
556
|
-
|
|
667
|
+
async () => {
|
|
668
|
+
const collectedDocumentIds = (
|
|
669
|
+
await db
|
|
670
|
+
.select({ id: schema.documents.id })
|
|
671
|
+
.from(schema.documents)
|
|
672
|
+
.where(
|
|
673
|
+
and(
|
|
674
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
675
|
+
eq(schema.documents.trashRootId, id),
|
|
676
|
+
isNotNull(schema.documents.trashedAt),
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
).map((document) => document.id);
|
|
680
|
+
const collectedDatabaseIds = await selectOwnedDatabaseIds(
|
|
681
|
+
db,
|
|
682
|
+
collectedDocumentIds,
|
|
683
|
+
ownerEmail,
|
|
684
|
+
).then((rows) => rows.map((database) => database.id));
|
|
685
|
+
return {
|
|
686
|
+
documentIds: collectedDocumentIds,
|
|
687
|
+
ownedDatabaseIds: collectedDatabaseIds,
|
|
688
|
+
lockDatabaseIds: [
|
|
689
|
+
...new Set([
|
|
690
|
+
...collectedDatabaseIds,
|
|
691
|
+
...(await selectMembershipDatabaseIds(
|
|
692
|
+
db,
|
|
693
|
+
collectedDocumentIds,
|
|
694
|
+
ownerEmail,
|
|
695
|
+
)),
|
|
696
|
+
]),
|
|
697
|
+
],
|
|
698
|
+
};
|
|
699
|
+
},
|
|
700
|
+
);
|
|
557
701
|
|
|
558
702
|
await db
|
|
559
703
|
.update(schema.documents)
|
|
@@ -633,12 +777,10 @@ export default defineAction({
|
|
|
633
777
|
if (systemDatabase?.systemRole) {
|
|
634
778
|
throw new Error("System Content database documents cannot be deleted");
|
|
635
779
|
}
|
|
636
|
-
const deleted = await
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
existing.ownerEmail as string,
|
|
641
|
-
),
|
|
780
|
+
const deleted = await trashDocumentSubtree(
|
|
781
|
+
db,
|
|
782
|
+
id,
|
|
783
|
+
existing.ownerEmail as string,
|
|
642
784
|
);
|
|
643
785
|
|
|
644
786
|
await writeAppState("refresh-signal", { ts: Date.now() });
|