@agent-native/core 0.102.2 → 0.103.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/core/CHANGELOG.md +6 -0
- package/corpus/core/docs/content/local-file-mode.mdx +99 -489
- package/corpus/core/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/corpus/core/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/corpus/core/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/corpus/core/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/corpus/core/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/corpus/core/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/corpus/core/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/corpus/core/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/corpus/core/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/corpus/core/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/corpus/core/docs/content/template-content.mdx +26 -42
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/content-local.ts +86 -18
- package/corpus/core/src/cli/index.ts +2 -2
- package/corpus/core/src/cli/skills-content/content-skill.ts +36 -25
- package/corpus/core/src/cli/skills.ts +47 -13
- package/corpus/core/src/local-artifacts/index.ts +65 -0
- package/corpus/templates/assets/app/components/library/GenerationPresetsPanel.tsx +325 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +12 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +21 -0
- package/corpus/templates/assets/app/i18n-data.ts +215 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +19 -479
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +1 -1
- package/corpus/templates/assets/app/routes/brand-kits.$id_.settings.tsx +446 -0
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +12 -2
- package/corpus/templates/clips/app/components/player/settings-panel.tsx +30 -12
- package/corpus/templates/clips/app/components/player/video-player.tsx +5 -1
- package/corpus/templates/clips/app/components/recorder/microphone-visualizer.tsx +22 -3
- package/corpus/templates/clips/app/i18n/ar-SA.ts +3 -2
- package/corpus/templates/clips/app/i18n/de-DE.ts +4 -2
- package/corpus/templates/clips/app/i18n/en-US.ts +3 -2
- package/corpus/templates/clips/app/i18n/es-ES.ts +4 -2
- package/corpus/templates/clips/app/i18n/fr-FR.ts +3 -2
- package/corpus/templates/clips/app/i18n/hi-IN.ts +3 -2
- package/corpus/templates/clips/app/i18n/ja-JP.ts +4 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +3 -2
- package/corpus/templates/clips/app/i18n/pt-BR.ts +4 -2
- package/corpus/templates/clips/app/i18n/zh-CN.ts +3 -2
- package/corpus/templates/clips/app/i18n/zh-TW.ts +3 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +27 -1
- package/corpus/templates/clips/server/lib/share-password.ts +6 -3
- package/corpus/templates/content/.agents/skills/content/SKILL.md +26 -39
- package/corpus/templates/content/AGENTS.md +25 -30
- package/corpus/templates/content/actions/_builder-docs-client.ts +23 -0
- package/corpus/templates/content/actions/_content-database-source-adapters.ts +21 -3
- package/corpus/templates/content/actions/_content-files.ts +338 -0
- package/corpus/templates/content/actions/_content-space-access.ts +135 -0
- package/corpus/templates/content/actions/_content-spaces.ts +640 -0
- package/corpus/templates/content/actions/_database-source-utils.ts +90 -7
- package/corpus/templates/content/actions/_database-utils.ts +7 -1
- package/corpus/templates/content/actions/_local-file-documents.ts +0 -9
- package/corpus/templates/content/actions/_local-folder-source.ts +53 -0
- package/corpus/templates/content/actions/_property-utils.ts +9 -4
- package/corpus/templates/content/actions/add-database-item.ts +108 -65
- package/corpus/templates/content/actions/backfill-content-files.ts +22 -0
- package/corpus/templates/content/actions/connect-local-folder-source.ts +221 -0
- package/corpus/templates/content/actions/create-content-database.ts +129 -3
- package/corpus/templates/content/actions/create-document.ts +65 -55
- package/corpus/templates/content/actions/create-inline-content-database.ts +6 -1
- package/corpus/templates/content/actions/delete-content-database.ts +3 -0
- package/corpus/templates/content/actions/delete-document.ts +7 -11
- package/corpus/templates/content/actions/disconnect-local-folder-source.ts +175 -0
- package/corpus/templates/content/actions/duplicate-database-item.ts +10 -0
- package/corpus/templates/content/actions/duplicate-database-items.ts +13 -0
- package/corpus/templates/content/actions/edit-document.ts +2 -28
- package/corpus/templates/content/actions/ensure-content-spaces.ts +26 -0
- package/corpus/templates/content/actions/export-content-source.ts +47 -9
- package/corpus/templates/content/actions/get-document.ts +0 -14
- package/corpus/templates/content/actions/import-content-source.ts +43 -3
- package/corpus/templates/content/actions/list-content-spaces.ts +103 -0
- package/corpus/templates/content/actions/list-documents.ts +12 -9
- package/corpus/templates/content/actions/move-document.ts +3 -14
- package/corpus/templates/content/actions/pull-document.ts +0 -23
- package/corpus/templates/content/actions/remove-local-file-source.ts +50 -29
- package/corpus/templates/content/actions/resolve-local-folder-conflict.ts +442 -0
- package/corpus/templates/content/actions/reveal-local-source-file.ts +6 -17
- package/corpus/templates/content/actions/search-documents.ts +0 -34
- package/corpus/templates/content/actions/share-local-file-document.ts +25 -1
- package/corpus/templates/content/actions/submit-content-database-form.ts +12 -0
- package/corpus/templates/content/actions/sync-local-folder-source.ts +731 -0
- package/corpus/templates/content/actions/sync-manifest-local-folder-source.ts +123 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +10 -1
- package/corpus/templates/content/actions/update-document.ts +0 -15
- package/corpus/templates/content/actions/view-screen.ts +2 -50
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +76 -62
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +70 -22
- package/corpus/templates/content/app/components/editor/database/grouping.ts +2 -1
- package/corpus/templates/content/app/components/editor/database/settings.tsx +23 -20
- package/corpus/templates/content/app/components/editor/database/shared.tsx +2 -0
- package/corpus/templates/content/app/components/editor/database/sidebar.tsx +330 -0
- package/corpus/templates/content/app/components/editor/database/types.ts +1 -0
- package/corpus/templates/content/app/components/editor/database/view-config.ts +3 -1
- package/corpus/templates/content/app/components/layout/Layout.tsx +25 -1
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +209 -113
- package/corpus/templates/content/app/components/sidebar/select-content-space.ts +31 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -0
- package/corpus/templates/content/app/hooks/use-content-spaces.ts +39 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +11 -0
- package/corpus/templates/content/app/i18n-data.ts +75 -0
- package/corpus/templates/content/app/routes/_app.local-files.tsx +174 -65
- package/corpus/templates/content/changelog/2026-07-16-content-callouts-can-now-be-reviewed-and-published-to-builder.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-heading-5-and-heading-6-are-available-from-the-editor-slash-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-16-organize-every-workspace-through-a-customizable-files-databa.md +6 -0
- package/corpus/templates/content/parity/matrix.md +2 -1
- package/corpus/templates/content/parity/matrix.ts +37 -1
- package/corpus/templates/content/scripts/dev-database.mjs +7 -5
- package/corpus/templates/content/server/db/schema.ts +113 -40
- package/corpus/templates/content/server/lib/document-context.ts +15 -1
- package/corpus/templates/content/server/lib/notion-sync.ts +6 -0
- package/corpus/templates/content/server/plugins/db.ts +153 -0
- package/corpus/templates/content/shared/api.ts +17 -3
- package/corpus/templates/content/shared/builder-mdx.ts +163 -16
- package/corpus/templates/design/.agents/skills/design-templates/SKILL.md +59 -0
- package/corpus/templates/design/AGENTS.md +6 -1
- package/corpus/templates/design/actions/create-design-from-template.ts +19 -7
- package/corpus/templates/design/actions/list-design-templates.ts +69 -21
- package/corpus/templates/design/actions/navigate.ts +2 -2
- package/corpus/templates/design/actions/view-screen.ts +15 -2
- package/corpus/templates/design/agent-native.app-skill.json +5 -0
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +281 -56
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +14 -2
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +3 -2
- package/corpus/templates/design/app/i18n/zh-TW.ts +3 -3
- package/corpus/templates/design/app/i18n-data.ts +21 -2
- package/corpus/templates/design/app/i18n-template-feature.ts +218 -10
- package/corpus/templates/design/app/pages/Index.tsx +195 -24
- package/corpus/templates/design/app/pages/Templates.tsx +144 -55
- package/corpus/templates/design/app/pages/design-editor/generation-prompt-directives.ts +12 -1
- package/corpus/templates/design/changelog/2026-07-14-templates-now-appear-in-the-new-design-picker-with-previews-.md +6 -0
- package/corpus/templates/design/changelog/2026-07-15-templates-now-open-immediately-after-copying-even-while-the-.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +1 -1
- package/corpus/templates/mail/AGENTS.md +23 -18
- package/corpus/templates/mail/actions/bulk-archive.ts +32 -28
- package/corpus/templates/mail/actions/mark-read.ts +131 -20
- package/corpus/templates/mail/actions/mark-thread-read.ts +1 -1
- package/corpus/templates/mail/actions/move-email.ts +30 -24
- package/corpus/templates/mail/actions/send-email.ts +73 -68
- package/corpus/templates/mail/changelog/2026-07-15-unread-inbox-cleanup-now-completes-reliably.md +6 -0
- package/corpus/templates/mail/server/handlers/emails.ts +218 -209
- package/corpus/templates/mail/server/lib/bulk-mark-read.ts +30 -0
- package/corpus/templates/mail/server/lib/email-state.ts +198 -113
- package/corpus/templates/mail/server/lib/google-auth.ts +218 -25
- package/corpus/templates/mail/server/lib/jobs.ts +69 -74
- package/corpus/templates/mail/server/lib/local-email-store.ts +265 -0
- package/corpus/templates/mail/server/plugins/agent-chat.ts +6 -0
- package/dist/cli/content-local.d.ts +1 -0
- package/dist/cli/content-local.d.ts.map +1 -1
- package/dist/cli/content-local.js +75 -17
- package/dist/cli/content-local.js.map +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/skills-content/content-skill.d.ts +1 -1
- package/dist/cli/skills-content/content-skill.d.ts.map +1 -1
- package/dist/cli/skills-content/content-skill.js +36 -25
- package/dist/cli/skills-content/content-skill.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +47 -14
- package/dist/cli/skills.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/local-artifacts/index.d.ts +10 -0
- package/dist/local-artifacts/index.d.ts.map +1 -1
- package/dist/local-artifacts/index.js +39 -0
- package/dist/local-artifacts/index.js.map +1 -1
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/docs/content/local-file-mode.mdx +99 -489
- package/docs/content/locales/ar-SA/local-file-mode.mdx +35 -490
- package/docs/content/locales/ar-SA/template-content.mdx +10 -24
- package/docs/content/locales/de-DE/local-file-mode.mdx +37 -491
- package/docs/content/locales/de-DE/template-content.mdx +10 -24
- package/docs/content/locales/es-ES/local-file-mode.mdx +35 -493
- package/docs/content/locales/es-ES/template-content.mdx +10 -24
- package/docs/content/locales/fr-FR/local-file-mode.mdx +38 -491
- package/docs/content/locales/fr-FR/template-content.mdx +10 -24
- package/docs/content/locales/hi-IN/local-file-mode.mdx +33 -492
- package/docs/content/locales/hi-IN/template-content.mdx +10 -24
- package/docs/content/locales/ja-JP/local-file-mode.mdx +36 -492
- package/docs/content/locales/ja-JP/template-content.mdx +10 -24
- package/docs/content/locales/ko-KR/local-file-mode.mdx +35 -490
- package/docs/content/locales/ko-KR/template-content.mdx +10 -24
- package/docs/content/locales/pt-BR/local-file-mode.mdx +35 -493
- package/docs/content/locales/pt-BR/template-content.mdx +10 -24
- package/docs/content/locales/zh-CN/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-CN/template-content.mdx +9 -24
- package/docs/content/locales/zh-TW/local-file-mode.mdx +31 -489
- package/docs/content/locales/zh-TW/template-content.mdx +9 -24
- package/docs/content/template-content.mdx +26 -42
- package/package.json +1 -1
- package/corpus/templates/assets/app/components/library/EditLibraryDialog.tsx +0 -126
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
4
|
+
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
import { schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
listContentOrganizationMemberships,
|
|
9
|
+
normalizeContentSpaceEmail,
|
|
10
|
+
resolveContentSpaceAccess,
|
|
11
|
+
} from "./_content-space-access.js";
|
|
12
|
+
import {
|
|
13
|
+
organizationContentSpaceId,
|
|
14
|
+
personalContentSpaceId,
|
|
15
|
+
} from "./_content-spaces.js";
|
|
16
|
+
|
|
17
|
+
type Db = any;
|
|
18
|
+
|
|
19
|
+
export type ContentFilesReconciliation = {
|
|
20
|
+
assignedSpaces: number;
|
|
21
|
+
insertedMemberships: number;
|
|
22
|
+
removedMemberships: number;
|
|
23
|
+
documents: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function contentFilesItemId(databaseId: string, documentId: string) {
|
|
27
|
+
return `content_database_item_${createHash("sha256")
|
|
28
|
+
.update(`${databaseId}:${documentId}`)
|
|
29
|
+
.digest("hex")
|
|
30
|
+
.slice(0, 32)}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function remapItemReferences(db: Db, replacements: Map<string, string>) {
|
|
34
|
+
if (!replacements.size) return;
|
|
35
|
+
const duplicateIds = [...replacements.keys()];
|
|
36
|
+
const remap = (column: any) => {
|
|
37
|
+
let expression = sql`CASE ${column}`;
|
|
38
|
+
for (const [duplicateId, canonicalId] of replacements)
|
|
39
|
+
expression = sql`${expression} WHEN ${duplicateId} THEN ${canonicalId}`;
|
|
40
|
+
return sql`${expression} ELSE ${column} END`;
|
|
41
|
+
};
|
|
42
|
+
for (const table of [
|
|
43
|
+
schema.contentSpaceCatalogItems,
|
|
44
|
+
schema.contentDatabaseBodyHydrationQueue,
|
|
45
|
+
schema.contentDatabaseSourceRows,
|
|
46
|
+
schema.contentDatabaseSourceChangeSets,
|
|
47
|
+
]) {
|
|
48
|
+
await db
|
|
49
|
+
.update(table)
|
|
50
|
+
.set({ databaseItemId: remap(table.databaseItemId) })
|
|
51
|
+
.where(inArray(table.databaseItemId, duplicateIds));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function reconcileDocuments(args: {
|
|
56
|
+
db: Db;
|
|
57
|
+
documents: Array<typeof schema.documents.$inferSelect>;
|
|
58
|
+
filesDatabases: Array<typeof schema.contentDatabases.$inferSelect>;
|
|
59
|
+
now: string;
|
|
60
|
+
}) {
|
|
61
|
+
const filesBySpace = new Map(
|
|
62
|
+
args.filesDatabases.map((database) => [database.spaceId, database]),
|
|
63
|
+
);
|
|
64
|
+
const filesDatabaseIds = args.filesDatabases.map((database) => database.id);
|
|
65
|
+
const filesBackingDocumentIds = new Set(
|
|
66
|
+
args.filesDatabases.map((database) => database.documentId),
|
|
67
|
+
);
|
|
68
|
+
const existingItems = filesDatabaseIds.length
|
|
69
|
+
? await args.db
|
|
70
|
+
.select()
|
|
71
|
+
.from(schema.contentDatabaseItems)
|
|
72
|
+
.where(
|
|
73
|
+
inArray(schema.contentDatabaseItems.databaseId, filesDatabaseIds),
|
|
74
|
+
)
|
|
75
|
+
: [];
|
|
76
|
+
const itemsByDocument = new Map<
|
|
77
|
+
string,
|
|
78
|
+
Array<typeof schema.contentDatabaseItems.$inferSelect>
|
|
79
|
+
>();
|
|
80
|
+
const nextPosition = new Map<string, number>();
|
|
81
|
+
for (const item of existingItems) {
|
|
82
|
+
const documentItems = itemsByDocument.get(item.documentId) ?? [];
|
|
83
|
+
documentItems.push(item);
|
|
84
|
+
itemsByDocument.set(item.documentId, documentItems);
|
|
85
|
+
nextPosition.set(
|
|
86
|
+
item.databaseId,
|
|
87
|
+
Math.max(nextPosition.get(item.databaseId) ?? 0, item.position + 1),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const deleteIds = new Set<string>();
|
|
92
|
+
const replacements = new Map<string, string>();
|
|
93
|
+
const inserts: Array<typeof schema.contentDatabaseItems.$inferInsert> = [];
|
|
94
|
+
for (const document of args.documents) {
|
|
95
|
+
const existing = itemsByDocument.get(document.id) ?? [];
|
|
96
|
+
if (filesBackingDocumentIds.has(document.id)) {
|
|
97
|
+
for (const item of existing) deleteIds.add(item.id);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const canonicalDatabase = filesBySpace.get(document.spaceId);
|
|
101
|
+
if (!canonicalDatabase) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Content space for document "${document.id}" has no Files database`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const canonicalItems = existing
|
|
107
|
+
.filter((item) => item.databaseId === canonicalDatabase.id)
|
|
108
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
109
|
+
const canonicalItemId =
|
|
110
|
+
canonicalItems[0]?.id ??
|
|
111
|
+
contentFilesItemId(canonicalDatabase.id, document.id);
|
|
112
|
+
for (const item of existing) {
|
|
113
|
+
if (item.databaseId !== canonicalDatabase.id) {
|
|
114
|
+
deleteIds.add(item.id);
|
|
115
|
+
replacements.set(item.id, canonicalItemId);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const duplicate of canonicalItems.slice(1)) {
|
|
119
|
+
deleteIds.add(duplicate.id);
|
|
120
|
+
replacements.set(duplicate.id, canonicalItemId);
|
|
121
|
+
}
|
|
122
|
+
if (canonicalItems.length === 0) {
|
|
123
|
+
const position = nextPosition.get(canonicalDatabase.id) ?? 0;
|
|
124
|
+
nextPosition.set(canonicalDatabase.id, position + 1);
|
|
125
|
+
inserts.push({
|
|
126
|
+
id: contentFilesItemId(canonicalDatabase.id, document.id),
|
|
127
|
+
ownerEmail: document.ownerEmail,
|
|
128
|
+
orgId: document.orgId,
|
|
129
|
+
databaseId: canonicalDatabase.id,
|
|
130
|
+
documentId: document.id,
|
|
131
|
+
position,
|
|
132
|
+
createdAt: args.now,
|
|
133
|
+
updatedAt: args.now,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
await remapItemReferences(args.db, replacements);
|
|
138
|
+
if (deleteIds.size) {
|
|
139
|
+
await args.db
|
|
140
|
+
.delete(schema.contentDatabaseItems)
|
|
141
|
+
.where(inArray(schema.contentDatabaseItems.id, [...deleteIds]));
|
|
142
|
+
}
|
|
143
|
+
if (inserts.length) {
|
|
144
|
+
await args.db
|
|
145
|
+
.insert(schema.contentDatabaseItems)
|
|
146
|
+
.values(inserts)
|
|
147
|
+
.onConflictDoNothing();
|
|
148
|
+
}
|
|
149
|
+
return { inserted: inserts.length, removed: deleteIds.size };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function ensureDocumentFilesMembership(
|
|
153
|
+
db: Db,
|
|
154
|
+
documentId: string,
|
|
155
|
+
now = new Date().toISOString(),
|
|
156
|
+
accessContext?: { userEmail?: string; orgId?: string },
|
|
157
|
+
) {
|
|
158
|
+
const [document] = await db
|
|
159
|
+
.select()
|
|
160
|
+
.from(schema.documents)
|
|
161
|
+
.where(
|
|
162
|
+
and(
|
|
163
|
+
eq(schema.documents.id, documentId),
|
|
164
|
+
accessFilter(schema.documents, schema.documentShares, accessContext),
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
if (!document) throw new Error(`Document "${documentId}" not found`);
|
|
168
|
+
if (!document.spaceId)
|
|
169
|
+
throw new Error(`Document "${documentId}" does not have a Content space`);
|
|
170
|
+
const filesDatabases = await db
|
|
171
|
+
.select()
|
|
172
|
+
.from(schema.contentDatabases)
|
|
173
|
+
.where(
|
|
174
|
+
and(
|
|
175
|
+
eq(schema.contentDatabases.systemRole, "files"),
|
|
176
|
+
eq(schema.contentDatabases.spaceId, document.spaceId),
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
return reconcileDocuments({ db, documents: [document], filesDatabases, now });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function ensureDocumentsFilesMembership(
|
|
183
|
+
db: Db,
|
|
184
|
+
documentIds: string[],
|
|
185
|
+
now = new Date().toISOString(),
|
|
186
|
+
ownerEmail?: string,
|
|
187
|
+
) {
|
|
188
|
+
const ids = [...new Set(documentIds)];
|
|
189
|
+
if (ids.length === 0) return { inserted: 0, removed: 0 };
|
|
190
|
+
const documents = await db
|
|
191
|
+
.select()
|
|
192
|
+
.from(schema.documents)
|
|
193
|
+
.where(
|
|
194
|
+
and(
|
|
195
|
+
inArray(schema.documents.id, ids),
|
|
196
|
+
ownerEmail
|
|
197
|
+
? or(
|
|
198
|
+
eq(schema.documents.ownerEmail, ownerEmail),
|
|
199
|
+
accessFilter(schema.documents, schema.documentShares),
|
|
200
|
+
)
|
|
201
|
+
: accessFilter(schema.documents, schema.documentShares),
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
if (documents.length !== ids.length) {
|
|
205
|
+
const found = new Set(documents.map((document: any) => document.id));
|
|
206
|
+
const missing = ids.filter((id) => !found.has(id));
|
|
207
|
+
throw new Error(`Documents not found: ${missing.join(", ")}`);
|
|
208
|
+
}
|
|
209
|
+
for (const document of documents) {
|
|
210
|
+
if (!document.spaceId) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Document "${document.id}" does not have a Content space`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const spaceIds: string[] = Array.from(
|
|
217
|
+
new Set<string>(documents.map((document: any) => String(document.spaceId))),
|
|
218
|
+
);
|
|
219
|
+
const filesDatabases = await db
|
|
220
|
+
.select()
|
|
221
|
+
.from(schema.contentDatabases)
|
|
222
|
+
.where(
|
|
223
|
+
and(
|
|
224
|
+
eq(schema.contentDatabases.systemRole, "files"),
|
|
225
|
+
or(
|
|
226
|
+
...spaceIds.map((spaceId) =>
|
|
227
|
+
eq(schema.contentDatabases.spaceId, spaceId),
|
|
228
|
+
),
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
return reconcileDocuments({ db, documents, filesDatabases, now });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function reconcileContentFilesMemberships(
|
|
236
|
+
db: Db,
|
|
237
|
+
userEmail: string,
|
|
238
|
+
): Promise<ContentFilesReconciliation> {
|
|
239
|
+
const email = normalizeContentSpaceEmail(userEmail);
|
|
240
|
+
const memberships = await listContentOrganizationMemberships(email);
|
|
241
|
+
const personalSpaceId = personalContentSpaceId(email);
|
|
242
|
+
const orgSpaceIds = new Map<string, string>();
|
|
243
|
+
for (const membership of memberships) {
|
|
244
|
+
const spaceId = organizationContentSpaceId(membership.orgId);
|
|
245
|
+
try {
|
|
246
|
+
await resolveContentSpaceAccess(spaceId, "editor");
|
|
247
|
+
orgSpaceIds.set(membership.orgId, spaceId);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (
|
|
250
|
+
error instanceof Error &&
|
|
251
|
+
error.message ===
|
|
252
|
+
`Editor access is required for Content space "${spaceId}"`
|
|
253
|
+
) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
throw error;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const accessibleSpaceIds = [personalSpaceId, ...orgSpaceIds.values()];
|
|
260
|
+
const now = new Date().toISOString();
|
|
261
|
+
const result: ContentFilesReconciliation = {
|
|
262
|
+
assignedSpaces: 0,
|
|
263
|
+
insertedMemberships: 0,
|
|
264
|
+
removedMemberships: 0,
|
|
265
|
+
documents: 0,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
await db.transaction(async (tx: Db) => {
|
|
269
|
+
const personalLegacyDocuments = await tx
|
|
270
|
+
.select({ id: schema.documents.id })
|
|
271
|
+
.from(schema.documents)
|
|
272
|
+
.where(
|
|
273
|
+
and(
|
|
274
|
+
eq(schema.documents.ownerEmail, email),
|
|
275
|
+
isNull(schema.documents.orgId),
|
|
276
|
+
isNull(schema.documents.spaceId),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
if (personalLegacyDocuments.length) {
|
|
280
|
+
await tx
|
|
281
|
+
.update(schema.documents)
|
|
282
|
+
.set({ spaceId: personalSpaceId, updatedAt: now })
|
|
283
|
+
.where(
|
|
284
|
+
inArray(
|
|
285
|
+
schema.documents.id,
|
|
286
|
+
personalLegacyDocuments.map((row: any) => row.id),
|
|
287
|
+
),
|
|
288
|
+
);
|
|
289
|
+
result.assignedSpaces += personalLegacyDocuments.length;
|
|
290
|
+
}
|
|
291
|
+
for (const [orgId, spaceId] of orgSpaceIds) {
|
|
292
|
+
const legacyDocuments = await tx
|
|
293
|
+
.select({ id: schema.documents.id })
|
|
294
|
+
.from(schema.documents)
|
|
295
|
+
.where(
|
|
296
|
+
and(
|
|
297
|
+
eq(schema.documents.orgId, orgId),
|
|
298
|
+
isNull(schema.documents.spaceId),
|
|
299
|
+
),
|
|
300
|
+
);
|
|
301
|
+
if (!legacyDocuments.length) continue;
|
|
302
|
+
await tx
|
|
303
|
+
.update(schema.documents)
|
|
304
|
+
.set({ spaceId, updatedAt: now })
|
|
305
|
+
.where(
|
|
306
|
+
inArray(
|
|
307
|
+
schema.documents.id,
|
|
308
|
+
legacyDocuments.map((row: any) => row.id),
|
|
309
|
+
),
|
|
310
|
+
);
|
|
311
|
+
result.assignedSpaces += legacyDocuments.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const documents = await tx
|
|
315
|
+
.select()
|
|
316
|
+
.from(schema.documents)
|
|
317
|
+
.where(inArray(schema.documents.spaceId, accessibleSpaceIds));
|
|
318
|
+
const filesDatabases = await tx
|
|
319
|
+
.select()
|
|
320
|
+
.from(schema.contentDatabases)
|
|
321
|
+
.where(
|
|
322
|
+
and(
|
|
323
|
+
eq(schema.contentDatabases.systemRole, "files"),
|
|
324
|
+
inArray(schema.contentDatabases.spaceId, accessibleSpaceIds),
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
const membershipResult = await reconcileDocuments({
|
|
328
|
+
db: tx,
|
|
329
|
+
documents,
|
|
330
|
+
filesDatabases,
|
|
331
|
+
now,
|
|
332
|
+
});
|
|
333
|
+
result.documents = documents.length;
|
|
334
|
+
result.insertedMemberships = membershipResult.inserted;
|
|
335
|
+
result.removedMemberships = membershipResult.removed;
|
|
336
|
+
});
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server/request-context";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
6
|
+
|
|
7
|
+
export type ContentSpaceRole = "viewer" | "editor" | "owner";
|
|
8
|
+
|
|
9
|
+
export type ContentSpaceAccess = {
|
|
10
|
+
space: typeof schema.contentSpaces.$inferSelect;
|
|
11
|
+
authority: { userEmail: string; orgId: string | null };
|
|
12
|
+
role: ContentSpaceRole;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function normalizeContentSpaceEmail(email: string): string {
|
|
16
|
+
const normalized = email.trim().toLowerCase();
|
|
17
|
+
if (!normalized) throw new Error("no authenticated user");
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getContentOrganizationMembership(
|
|
22
|
+
orgId: string,
|
|
23
|
+
userEmail: string,
|
|
24
|
+
): Promise<{ role: string; name: string; createdBy: string } | null> {
|
|
25
|
+
const result = await getDbExec().execute({
|
|
26
|
+
sql: `SELECT m.role AS role, o.name AS name, o.created_by AS "createdBy"
|
|
27
|
+
FROM org_members m
|
|
28
|
+
INNER JOIN organizations o ON o.id = m.org_id
|
|
29
|
+
WHERE m.org_id = ? AND LOWER(m.email) = ?
|
|
30
|
+
LIMIT 1`,
|
|
31
|
+
args: [orgId, normalizeContentSpaceEmail(userEmail)],
|
|
32
|
+
});
|
|
33
|
+
const row = result.rows[0] as
|
|
34
|
+
| { role?: unknown; name?: unknown; createdBy?: unknown }
|
|
35
|
+
| undefined;
|
|
36
|
+
if (
|
|
37
|
+
!row ||
|
|
38
|
+
typeof row.name !== "string" ||
|
|
39
|
+
typeof row.createdBy !== "string"
|
|
40
|
+
) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
role: String(row.role ?? "member").toLowerCase(),
|
|
45
|
+
name: row.name,
|
|
46
|
+
createdBy: row.createdBy,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function listContentOrganizationMemberships(userEmail: string) {
|
|
51
|
+
let result;
|
|
52
|
+
try {
|
|
53
|
+
result = await getDbExec().execute({
|
|
54
|
+
sql: `SELECT m.org_id AS "orgId", m.role AS role, o.name AS name,
|
|
55
|
+
o.created_by AS "createdBy"
|
|
56
|
+
FROM org_members m
|
|
57
|
+
INNER JOIN organizations o ON o.id = m.org_id
|
|
58
|
+
WHERE LOWER(m.email) = ?
|
|
59
|
+
ORDER BY m.org_id ASC`,
|
|
60
|
+
args: [normalizeContentSpaceEmail(userEmail)],
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof Error && error.message.includes("no such table")) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
return result.rows
|
|
69
|
+
.map((row: any) => ({
|
|
70
|
+
orgId: typeof row.orgId === "string" ? row.orgId : row.org_id,
|
|
71
|
+
role: String(row.role ?? "member").toLowerCase(),
|
|
72
|
+
name: row.name,
|
|
73
|
+
createdBy: row.createdBy ?? row.created_by,
|
|
74
|
+
}))
|
|
75
|
+
.filter(
|
|
76
|
+
(
|
|
77
|
+
row,
|
|
78
|
+
): row is {
|
|
79
|
+
orgId: string;
|
|
80
|
+
role: string;
|
|
81
|
+
name: string;
|
|
82
|
+
createdBy: string;
|
|
83
|
+
} =>
|
|
84
|
+
typeof row.orgId === "string" &&
|
|
85
|
+
typeof row.name === "string" &&
|
|
86
|
+
typeof row.createdBy === "string",
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function resolveContentSpaceAccess(
|
|
91
|
+
spaceId: string,
|
|
92
|
+
requiredRole: "viewer" | "editor" = "viewer",
|
|
93
|
+
): Promise<ContentSpaceAccess> {
|
|
94
|
+
const userEmail = getRequestUserEmail();
|
|
95
|
+
if (!userEmail) throw new Error("no authenticated user");
|
|
96
|
+
const normalizedUserEmail = normalizeContentSpaceEmail(userEmail);
|
|
97
|
+
const [space] = await getDb()
|
|
98
|
+
.select()
|
|
99
|
+
.from(schema.contentSpaces)
|
|
100
|
+
.where(eq(schema.contentSpaces.id, spaceId));
|
|
101
|
+
if (!space || space.archivedAt)
|
|
102
|
+
throw new Error(`Content space "${spaceId}" not found`);
|
|
103
|
+
|
|
104
|
+
if (!space.orgId) {
|
|
105
|
+
if (normalizeContentSpaceEmail(space.ownerEmail) !== normalizedUserEmail) {
|
|
106
|
+
throw new Error(`Not authorized for Content space "${spaceId}"`);
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
space,
|
|
110
|
+
authority: { userEmail: normalizedUserEmail, orgId: null },
|
|
111
|
+
role: "owner",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const membership = await getContentOrganizationMembership(
|
|
116
|
+
space.orgId,
|
|
117
|
+
normalizedUserEmail,
|
|
118
|
+
);
|
|
119
|
+
if (!membership)
|
|
120
|
+
throw new Error(`Not authorized for Content space "${spaceId}"`);
|
|
121
|
+
const role: ContentSpaceRole =
|
|
122
|
+
membership.role === "owner"
|
|
123
|
+
? "owner"
|
|
124
|
+
: membership.role === "admin"
|
|
125
|
+
? "editor"
|
|
126
|
+
: "viewer";
|
|
127
|
+
if (requiredRole === "editor" && role === "viewer") {
|
|
128
|
+
throw new Error(`Editor access is required for Content space "${spaceId}"`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
space,
|
|
132
|
+
authority: { userEmail: normalizedUserEmail, orgId: space.orgId },
|
|
133
|
+
role,
|
|
134
|
+
};
|
|
135
|
+
}
|