@ai-matrx/media 0.8.0 → 0.9.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.
- package/CHANGELOG.md +21 -0
- package/dist/files/engine/api/assets.d.ts +223 -0
- package/dist/files/engine/api/assets.js +124 -0
- package/dist/files/engine/api/assets.js.map +1 -0
- package/dist/files/engine/api/direct.d.ts +43 -0
- package/dist/files/engine/api/direct.js +73 -0
- package/dist/files/engine/api/direct.js.map +1 -0
- package/dist/files/engine/api/fileOrganization.d.ts +34 -0
- package/dist/files/engine/api/fileOrganization.js +38 -0
- package/dist/files/engine/api/fileOrganization.js.map +1 -0
- package/dist/files/engine/api/files.d.ts +249 -0
- package/dist/files/engine/api/files.js +231 -0
- package/dist/files/engine/api/files.js.map +1 -0
- package/dist/files/engine/api/folders.d.ts +63 -0
- package/dist/files/engine/api/folders.js +56 -0
- package/dist/files/engine/api/folders.js.map +1 -0
- package/dist/files/engine/api/office.d.ts +27 -0
- package/dist/files/engine/api/office.js +19 -0
- package/dist/files/engine/api/office.js.map +1 -0
- package/dist/files/engine/api/permissions.d.ts +37 -0
- package/dist/files/engine/api/permissions.js +109 -0
- package/dist/files/engine/api/permissions.js.map +1 -0
- package/dist/files/engine/api/versions.d.ts +26 -0
- package/dist/files/engine/api/versions.js +37 -0
- package/dist/files/engine/api/versions.js.map +1 -0
- package/dist/files/engine/cache/idb-store.d.ts +95 -0
- package/dist/files/engine/cache/idb-store.js +172 -0
- package/dist/files/engine/cache/idb-store.js.map +1 -0
- package/dist/files/engine/cache/policy.d.ts +14 -0
- package/dist/files/engine/cache/policy.js +40 -0
- package/dist/files/engine/cache/policy.js.map +1 -0
- package/dist/files/engine/cache/register-service-worker.d.ts +57 -0
- package/dist/files/engine/cache/register-service-worker.js +80 -0
- package/dist/files/engine/cache/register-service-worker.js.map +1 -0
- package/dist/files/engine/db-types.d.ts +48865 -0
- package/dist/files/engine/db-types.js +1 -0
- package/dist/files/engine/db-types.js.map +1 -0
- package/dist/files/engine/filesDb.d.ts +1913 -0
- package/dist/files/engine/filesDb.js +28 -0
- package/dist/files/engine/filesDb.js.map +1 -0
- package/dist/files/engine/handler/handler.d.ts +82 -0
- package/dist/files/engine/handler/handler.js +124 -0
- package/dist/files/engine/handler/handler.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFile.d.ts +17 -0
- package/dist/files/engine/handler/hooks/useFile.js +75 -0
- package/dist/files/engine/handler/hooks/useFile.js.map +1 -0
- package/dist/files/engine/handler/hooks/useFileUpload.d.ts +76 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js +67 -0
- package/dist/files/engine/handler/hooks/useFileUpload.js.map +1 -0
- package/dist/files/engine/handler/input/normalize.d.ts +14 -0
- package/dist/files/engine/handler/input/normalize.js +365 -0
- package/dist/files/engine/handler/input/normalize.js.map +1 -0
- package/dist/files/engine/handler/intelligence/access.d.ts +35 -0
- package/dist/files/engine/handler/intelligence/access.js +85 -0
- package/dist/files/engine/handler/intelligence/access.js.map +1 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.d.ts +21 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js +67 -0
- package/dist/files/engine/handler/intelligence/magic-bytes.js.map +1 -0
- package/dist/files/engine/handler/output/target.d.ts +19 -0
- package/dist/files/engine/handler/output/target.js +222 -0
- package/dist/files/engine/handler/output/target.js.map +1 -0
- package/dist/files/engine/handler/resolver.d.ts +34 -0
- package/dist/files/engine/handler/resolver.js +133 -0
- package/dist/files/engine/handler/resolver.js.map +1 -0
- package/dist/files/engine/handler/types.d.ts +367 -0
- package/dist/files/engine/handler/types.js +1 -0
- package/dist/files/engine/handler/types.js.map +1 -0
- package/dist/files/engine/handler/upload.d.ts +32 -0
- package/dist/files/engine/handler/upload.js +295 -0
- package/dist/files/engine/handler/upload.js.map +1 -0
- package/dist/files/engine/handler/utils/classify.d.ts +14 -0
- package/dist/files/engine/handler/utils/classify.js +27 -0
- package/dist/files/engine/handler/utils/classify.js.map +1 -0
- package/dist/files/engine/handler/utils/prefer-locator.d.ts +32 -0
- package/dist/files/engine/handler/utils/prefer-locator.js +34 -0
- package/dist/files/engine/handler/utils/prefer-locator.js.map +1 -0
- package/dist/files/engine/handler/utils/python-base.d.ts +129 -0
- package/dist/files/engine/handler/utils/python-base.js +60 -0
- package/dist/files/engine/handler/utils/python-base.js.map +1 -0
- package/dist/files/engine/hooks/blob-cache.d.ts +130 -0
- package/dist/files/engine/hooks/blob-cache.js +162 -0
- package/dist/files/engine/hooks/blob-cache.js.map +1 -0
- package/dist/files/engine/hooks/office-extraction-cache.d.ts +28 -0
- package/dist/files/engine/hooks/office-extraction-cache.js +67 -0
- package/dist/files/engine/hooks/office-extraction-cache.js.map +1 -0
- package/dist/files/engine/host/configure.d.ts +166 -0
- package/dist/files/engine/host/configure.js +31 -0
- package/dist/files/engine/host/configure.js.map +1 -0
- package/dist/files/engine/host/org.d.ts +8 -0
- package/dist/files/engine/host/org.js +16 -0
- package/dist/files/engine/host/org.js.map +1 -0
- package/dist/files/engine/host/python-client.d.ts +60 -0
- package/dist/files/engine/host/python-client.js +46 -0
- package/dist/files/engine/host/python-client.js.map +1 -0
- package/dist/files/engine/host/share-links.d.ts +20 -0
- package/dist/files/engine/host/share-links.js +32 -0
- package/dist/files/engine/host/share-links.js.map +1 -0
- package/dist/files/engine/host/store.d.ts +24 -0
- package/dist/files/engine/host/store.js +23 -0
- package/dist/files/engine/host/store.js.map +1 -0
- package/dist/files/engine/host/supabase.d.ts +3808 -0
- package/dist/files/engine/host/supabase.js +33 -0
- package/dist/files/engine/host/supabase.js.map +1 -0
- package/dist/files/engine/host/toast.d.ts +14 -0
- package/dist/files/engine/host/toast.js +20 -0
- package/dist/files/engine/host/toast.js.map +1 -0
- package/dist/files/engine/host/typed-client.d.ts +136 -0
- package/dist/files/engine/host/typed-client.js +66 -0
- package/dist/files/engine/host/typed-client.js.map +1 -0
- package/dist/files/engine/index.d.ts +22 -0
- package/dist/files/engine/index.js +49 -0
- package/dist/files/engine/index.js.map +1 -0
- package/dist/files/engine/media/our-file-sources.d.ts +39 -0
- package/dist/files/engine/media/our-file-sources.js +83 -0
- package/dist/files/engine/media/our-file-sources.js.map +1 -0
- package/dist/files/engine/media/signed-url.d.ts +1 -0
- package/dist/files/engine/media/signed-url.js +6 -0
- package/dist/files/engine/media/signed-url.js.map +1 -0
- package/dist/files/engine/redux/converters.d.ts +73 -0
- package/dist/files/engine/redux/converters.js +286 -0
- package/dist/files/engine/redux/converters.js.map +1 -0
- package/dist/files/engine/redux/file-hydration.d.ts +15 -0
- package/dist/files/engine/redux/file-hydration.js +46 -0
- package/dist/files/engine/redux/file-hydration.js.map +1 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.d.ts +5 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js +28 -0
- package/dist/files/engine/redux/file-tree-auth-boundary.js.map +1 -0
- package/dist/files/engine/redux/file-tree-timeout.d.ts +32 -0
- package/dist/files/engine/redux/file-tree-timeout.js +73 -0
- package/dist/files/engine/redux/file-tree-timeout.js.map +1 -0
- package/dist/files/engine/redux/mutation-toast-middleware.d.ts +27 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js +103 -0
- package/dist/files/engine/redux/mutation-toast-middleware.js.map +1 -0
- package/dist/files/engine/redux/realtime-middleware.d.ts +61 -0
- package/dist/files/engine/redux/realtime-middleware.js +431 -0
- package/dist/files/engine/redux/realtime-middleware.js.map +1 -0
- package/dist/files/engine/redux/request-ledger.d.ts +68 -0
- package/dist/files/engine/redux/request-ledger.js +66 -0
- package/dist/files/engine/redux/request-ledger.js.map +1 -0
- package/dist/files/engine/redux/selectors.d.ts +3254 -0
- package/dist/files/engine/redux/selectors.js +428 -0
- package/dist/files/engine/redux/selectors.js.map +1 -0
- package/dist/files/engine/redux/slice.d.ts +157 -0
- package/dist/files/engine/redux/slice.js +836 -0
- package/dist/files/engine/redux/slice.js.map +1 -0
- package/dist/files/engine/redux/thunks.d.ts +449 -0
- package/dist/files/engine/redux/thunks.js +1684 -0
- package/dist/files/engine/redux/thunks.js.map +1 -0
- package/dist/files/engine/redux/tree-utils.d.ts +90 -0
- package/dist/files/engine/redux/tree-utils.js +200 -0
- package/dist/files/engine/redux/tree-utils.js.map +1 -0
- package/dist/files/engine/support/claimsUser.d.ts +77 -0
- package/dist/files/engine/support/claimsUser.js +48 -0
- package/dist/files/engine/support/claimsUser.js.map +1 -0
- package/dist/files/engine/support/datetime.d.ts +6 -0
- package/dist/files/engine/support/datetime.js +15 -0
- package/dist/files/engine/support/datetime.js.map +1 -0
- package/dist/files/engine/support/document-visibility.d.ts +14 -0
- package/dist/files/engine/support/document-visibility.js +28 -0
- package/dist/files/engine/support/document-visibility.js.map +1 -0
- package/dist/files/engine/support/logger.d.ts +28 -0
- package/dist/files/engine/support/logger.js +36 -0
- package/dist/files/engine/support/logger.js.map +1 -0
- package/dist/files/engine/types.d.ts +1081 -0
- package/dist/files/engine/types.js +30 -0
- package/dist/files/engine/types.js.map +1 -0
- package/dist/files/engine/upload/cloudUpload.d.ts +182 -0
- package/dist/files/engine/upload/cloudUpload.js +418 -0
- package/dist/files/engine/upload/cloudUpload.js.map +1 -0
- package/dist/files/engine/upload/tusUpload.d.ts +100 -0
- package/dist/files/engine/upload/tusUpload.js +258 -0
- package/dist/files/engine/upload/tusUpload.js.map +1 -0
- package/dist/files/engine/upload/uploadDedupGuard.d.ts +58 -0
- package/dist/files/engine/upload/uploadDedupGuard.js +41 -0
- package/dist/files/engine/upload/uploadDedupGuard.js.map +1 -0
- package/dist/files/engine/upload/uploadGuardOpeners.d.ts +70 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js +54 -0
- package/dist/files/engine/upload/uploadGuardOpeners.js.map +1 -0
- package/dist/files/engine/utils/file-types.d.ts +243 -0
- package/dist/files/engine/utils/file-types.js +1722 -0
- package/dist/files/engine/utils/file-types.js.map +1 -0
- package/dist/files/engine/utils/user-visible.d.ts +133 -0
- package/dist/files/engine/utils/user-visible.js +115 -0
- package/dist/files/engine/utils/user-visible.js.map +1 -0
- package/package.json +392 -8
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { toVisibility, fileIdToMediaRef, urlToMediaRef } from "../../../files.js";
|
|
2
|
+
function toPermissionLevel(raw) {
|
|
3
|
+
return raw === "write" || raw === "admin" ? raw : "read";
|
|
4
|
+
}
|
|
5
|
+
function canonicalLevelToFileLevel(raw) {
|
|
6
|
+
if (raw === "admin") return "admin";
|
|
7
|
+
if (raw === "editor") return "write";
|
|
8
|
+
return "read";
|
|
9
|
+
}
|
|
10
|
+
function toResourceType(raw) {
|
|
11
|
+
return raw === "folder" ? "folder" : "file";
|
|
12
|
+
}
|
|
13
|
+
function toMetadataObject(raw) {
|
|
14
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
function dbRowToCloudFile(row) {
|
|
20
|
+
return {
|
|
21
|
+
id: row.id,
|
|
22
|
+
// `files.files` is canonical now: owner lives in `created_by` (trigger-
|
|
23
|
+
// stamped), not the old `owner_id`. Domain keeps the `ownerId` field name.
|
|
24
|
+
ownerId: row.created_by,
|
|
25
|
+
organizationId: row.organization_id,
|
|
26
|
+
filePath: row.file_path,
|
|
27
|
+
fileName: row.file_name,
|
|
28
|
+
mimeType: row.mime_type,
|
|
29
|
+
// Phase 0 rename: `cld_files.file_size` → `cld_files.size_bytes`.
|
|
30
|
+
// See docs/PYTHON_UPDATES.md §3. The Supabase generated row type
|
|
31
|
+
// already carries the new column name.
|
|
32
|
+
fileSize: row.size_bytes,
|
|
33
|
+
checksum: row.checksum,
|
|
34
|
+
visibility: toVisibility(row.visibility),
|
|
35
|
+
currentVersion: row.current_version,
|
|
36
|
+
parentFolderId: row.parent_folder_id,
|
|
37
|
+
metadata: toMetadataObject(row.metadata),
|
|
38
|
+
createdAt: row.created_at,
|
|
39
|
+
updatedAt: row.updated_at,
|
|
40
|
+
deletedAt: row.deleted_at,
|
|
41
|
+
// The DB has no computed-URL columns (public_url / url / cdn_url /
|
|
42
|
+
// download_url are all server-computed on the REST path). Direct-DB
|
|
43
|
+
// reads (Supabase realtime, SSR) leave these null; the resolver builds
|
|
44
|
+
// the durable URL from the file id on demand via useFileSrc.
|
|
45
|
+
publicUrl: null,
|
|
46
|
+
url: null,
|
|
47
|
+
cdnUrl: null,
|
|
48
|
+
downloadUrl: null,
|
|
49
|
+
// The DB no longer has a thumbnail_url column either (Phase 1b dropped
|
|
50
|
+
// it); resolved server-side from the variants store on the REST path.
|
|
51
|
+
// Direct-DB-read callers should fall back to `useFileAsset(fileId)`
|
|
52
|
+
// and read `asset.variants["thumbnail_url"].url`.
|
|
53
|
+
thumbnailUrl: null,
|
|
54
|
+
source: { kind: "real" },
|
|
55
|
+
// Lineage + dimension columns exist on the DB row but were never mapped,
|
|
56
|
+
// so realtime/SSR-sourced records lost derivedFrom + width/height that
|
|
57
|
+
// REST-sourced records keep. Map them for shape parity.
|
|
58
|
+
parentFileId: row.parent_file_id ?? null,
|
|
59
|
+
derivationKind: row.derivation_kind ?? null,
|
|
60
|
+
derivationMetadata: row.derivation_metadata ?? null,
|
|
61
|
+
// Dedup-pyramid columns from Phase 2.0. Null on rows uploaded before
|
|
62
|
+
// the dedup consolidation script ran or on rows that have never been
|
|
63
|
+
// identified as duplicates / never had their canonical extract set.
|
|
64
|
+
duplicateOfFileId: row.duplicate_of_file_id ?? null,
|
|
65
|
+
canonicalProcessedDocumentId: row.canonical_processed_document_id ?? null,
|
|
66
|
+
originDeviceId: row.origin_device_id ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function apiFileRecordToCloudFile(row) {
|
|
70
|
+
const extras = row;
|
|
71
|
+
return {
|
|
72
|
+
id: row.id,
|
|
73
|
+
ownerId: row.owner_id,
|
|
74
|
+
organizationId: extras.organization_id ?? null,
|
|
75
|
+
filePath: row.file_path,
|
|
76
|
+
fileName: row.file_name,
|
|
77
|
+
mimeType: row.mime_type ?? null,
|
|
78
|
+
// Phase 0 rename: `FileRecord.file_size` → `FileRecord.size_bytes`.
|
|
79
|
+
// The Python OpenAPI schema has been regenerated. See
|
|
80
|
+
// docs/PYTHON_UPDATES.md §3.
|
|
81
|
+
fileSize: row.size_bytes ?? null,
|
|
82
|
+
checksum: row.checksum ?? null,
|
|
83
|
+
visibility: toVisibility(row.visibility),
|
|
84
|
+
currentVersion: row.current_version ?? 1,
|
|
85
|
+
parentFolderId: row.parent_folder_id ?? null,
|
|
86
|
+
metadata: toMetadataObject(row.metadata),
|
|
87
|
+
createdAt: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
88
|
+
updatedAt: row.updated_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
+
deletedAt: row.deleted_at ?? null,
|
|
90
|
+
// CDN URL for visibility="public" files when the server has the CDN
|
|
91
|
+
// feature enabled. Includes a ?v=<checksum[:8]> cache-buster.
|
|
92
|
+
publicUrl: row.public_url ?? null,
|
|
93
|
+
// The DURABLE URL envelope (api-types.ts FileRecord): `url` is the
|
|
94
|
+
// canonical renderable locator (CDN for public, durable download route
|
|
95
|
+
// for private), `cdn_url` public-only, `download_url` attachment
|
|
96
|
+
// disposition. None of them expire.
|
|
97
|
+
url: row.url ?? null,
|
|
98
|
+
cdnUrl: row.cdn_url ?? null,
|
|
99
|
+
downloadUrl: row.download_url ?? null,
|
|
100
|
+
// Phase 1b: backend-rendered thumbnail (every file gets one). The
|
|
101
|
+
// wire field is resolved server-side from the variants store now
|
|
102
|
+
// that `cld_files.thumbnail_url` column has been dropped.
|
|
103
|
+
thumbnailUrl: row.thumbnail_url ?? null,
|
|
104
|
+
source: { kind: "real" },
|
|
105
|
+
parentFileId: extras.parent_file_id ?? null,
|
|
106
|
+
derivationKind: extras.derivation_kind ?? null,
|
|
107
|
+
derivationMetadata: extras.derivation_metadata ?? null,
|
|
108
|
+
duplicateOfFileId: extras.duplicate_of_file_id ?? null,
|
|
109
|
+
canonicalProcessedDocumentId: extras.canonical_processed_document_id ?? null
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function dbRowToCloudFolder(row) {
|
|
113
|
+
return {
|
|
114
|
+
id: row.id,
|
|
115
|
+
// Canonical `files.folders`: owner is `created_by`, not `owner_id`.
|
|
116
|
+
ownerId: row.created_by,
|
|
117
|
+
folderPath: row.folder_path,
|
|
118
|
+
folderName: row.folder_name,
|
|
119
|
+
parentId: row.parent_id,
|
|
120
|
+
visibility: toVisibility(row.visibility),
|
|
121
|
+
metadata: toMetadataObject(row.metadata),
|
|
122
|
+
createdAt: row.created_at,
|
|
123
|
+
updatedAt: row.updated_at,
|
|
124
|
+
deletedAt: row.deleted_at,
|
|
125
|
+
source: { kind: "real" }
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function dbRowToCloudFileVersion(row) {
|
|
129
|
+
return {
|
|
130
|
+
id: row.id,
|
|
131
|
+
fileId: row.file_id,
|
|
132
|
+
versionNumber: row.version_number,
|
|
133
|
+
// Phase 0 rename — see `dbRowToCloudFile` above.
|
|
134
|
+
fileSize: row.size_bytes,
|
|
135
|
+
checksum: row.checksum,
|
|
136
|
+
// Who saved THIS version is not recorded: files.file_versions has no
|
|
137
|
+
// updated_by, and its created_by is rewritten to the FILE's owner
|
|
138
|
+
// (component of files.files, db-rules §6d-1) — so it is unknown, never
|
|
139
|
+
// the owner presented as the author.
|
|
140
|
+
createdBy: null,
|
|
141
|
+
createdAt: row.created_at,
|
|
142
|
+
changeSummary: row.change_summary
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function dbRowToCloudFilePermission(row) {
|
|
146
|
+
const isPublicGrant = row.is_public === true && row.granted_to_user_id == null && row.granted_to_organization_id == null;
|
|
147
|
+
const isOrgGrant = row.granted_to_organization_id != null;
|
|
148
|
+
const granteeType = isPublicGrant ? "public" : isOrgGrant ? "group" : "user";
|
|
149
|
+
const granteeId = isPublicGrant ? row.id : row.granted_to_user_id ?? row.granted_to_organization_id ?? row.id;
|
|
150
|
+
return {
|
|
151
|
+
id: row.id,
|
|
152
|
+
resourceId: row.resource_id,
|
|
153
|
+
resourceType: toResourceType(row.resource_type),
|
|
154
|
+
granteeId,
|
|
155
|
+
granteeType,
|
|
156
|
+
permissionLevel: canonicalLevelToFileLevel(row.permission_level),
|
|
157
|
+
grantedBy: row.created_by,
|
|
158
|
+
grantedAt: row.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
159
|
+
expiresAt: row.expires_at
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function rpcPermissionRowToCloudFilePermission(row) {
|
|
163
|
+
const granteeType = row.grantee_type === "organization" ? "group" : "user";
|
|
164
|
+
return {
|
|
165
|
+
id: row.grantee_id,
|
|
166
|
+
resourceId: row.resource_id,
|
|
167
|
+
resourceType: toResourceType(row.resource_type),
|
|
168
|
+
granteeId: row.grantee_id,
|
|
169
|
+
granteeType,
|
|
170
|
+
permissionLevel: toPermissionLevel(row.permission_level),
|
|
171
|
+
grantedBy: row.granted_by,
|
|
172
|
+
grantedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
173
|
+
expiresAt: row.expires_at
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function canonicalShareLinkToCloudShareLink(link, resourceType, resourceId) {
|
|
177
|
+
return {
|
|
178
|
+
id: link.id,
|
|
179
|
+
resourceId,
|
|
180
|
+
resourceType,
|
|
181
|
+
shareToken: link.token,
|
|
182
|
+
// File links are minted viewer/editor; anything above editor still means
|
|
183
|
+
// "can modify" for display purposes.
|
|
184
|
+
permissionLevel: link.permissionLevel === "viewer" ? "viewer" : "editor",
|
|
185
|
+
label: link.label,
|
|
186
|
+
createdAt: link.createdAt,
|
|
187
|
+
expiresAt: link.expiresAt,
|
|
188
|
+
maxUses: link.maxUses,
|
|
189
|
+
useCount: link.useCount,
|
|
190
|
+
isActive: link.isActive
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function str(row, key) {
|
|
194
|
+
const v = row[key];
|
|
195
|
+
return typeof v === "string" ? v : null;
|
|
196
|
+
}
|
|
197
|
+
function num(row, key) {
|
|
198
|
+
const v = row[key];
|
|
199
|
+
return typeof v === "number" ? v : null;
|
|
200
|
+
}
|
|
201
|
+
function parseCloudTreeRow(raw) {
|
|
202
|
+
if (!raw || typeof raw !== "object") return null;
|
|
203
|
+
const row = raw;
|
|
204
|
+
const id = str(row, "id");
|
|
205
|
+
if (!id) return null;
|
|
206
|
+
const ownerId = str(row, "owner_id") ?? str(row, "created_by");
|
|
207
|
+
if (!ownerId) return null;
|
|
208
|
+
const explicitKind = str(row, "kind");
|
|
209
|
+
const hasFolderName = typeof row.folder_name === "string";
|
|
210
|
+
const hasFileName = typeof row.file_name === "string";
|
|
211
|
+
const isFolder = explicitKind === "folder" || hasFolderName && !hasFileName;
|
|
212
|
+
const created = str(row, "created_at") ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
213
|
+
const updated = str(row, "updated_at") ?? created;
|
|
214
|
+
const visibility = toVisibility(str(row, "visibility") ?? "personal");
|
|
215
|
+
const effPerm = str(row, "effective_permission");
|
|
216
|
+
const folderPath = str(row, "folder_path") ?? str(row, "path") ?? "";
|
|
217
|
+
const folderName = str(row, "folder_name") ?? str(row, "name") ?? "";
|
|
218
|
+
const filePath = str(row, "file_path") ?? str(row, "path") ?? "";
|
|
219
|
+
const fileName = str(row, "file_name") ?? str(row, "name") ?? "";
|
|
220
|
+
const parentFolderId = str(row, "parent_folder_id") ?? str(row, "parent_id");
|
|
221
|
+
const sizeBytes = num(row, "size_bytes") ?? num(row, "file_size");
|
|
222
|
+
if (isFolder) {
|
|
223
|
+
return {
|
|
224
|
+
kind: "folder",
|
|
225
|
+
id,
|
|
226
|
+
folder_path: folderPath,
|
|
227
|
+
folder_name: folderName,
|
|
228
|
+
parent_id: str(row, "parent_id"),
|
|
229
|
+
visibility,
|
|
230
|
+
effective_permission: effPerm ? toPermissionLevel(effPerm) : null,
|
|
231
|
+
owner_id: ownerId,
|
|
232
|
+
created_at: created,
|
|
233
|
+
updated_at: updated,
|
|
234
|
+
deleted_at: str(row, "deleted_at"),
|
|
235
|
+
metadata: toMetadataObject(row.metadata)
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
kind: "file",
|
|
240
|
+
id,
|
|
241
|
+
file_path: filePath,
|
|
242
|
+
file_name: fileName,
|
|
243
|
+
parent_folder_id: parentFolderId,
|
|
244
|
+
mime_type: str(row, "mime_type"),
|
|
245
|
+
size_bytes: sizeBytes,
|
|
246
|
+
visibility,
|
|
247
|
+
current_version: num(row, "current_version") ?? 1,
|
|
248
|
+
effective_permission: effPerm ? toPermissionLevel(effPerm) : null,
|
|
249
|
+
owner_id: ownerId,
|
|
250
|
+
created_at: created,
|
|
251
|
+
updated_at: updated,
|
|
252
|
+
deleted_at: str(row, "deleted_at"),
|
|
253
|
+
origin_device_id: str(row, "origin_device_id"),
|
|
254
|
+
metadata: toMetadataObject(row.metadata)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function parseCloudTreeRows(raw) {
|
|
258
|
+
if (!Array.isArray(raw)) return [];
|
|
259
|
+
const out = [];
|
|
260
|
+
for (const item of raw) {
|
|
261
|
+
const parsed = parseCloudTreeRow(item);
|
|
262
|
+
if (parsed) out.push(parsed);
|
|
263
|
+
}
|
|
264
|
+
return out;
|
|
265
|
+
}
|
|
266
|
+
function cloudFileToMediaRef(file) {
|
|
267
|
+
const ref = { file_id: file.id };
|
|
268
|
+
if (file.mimeType) ref.mime_type = file.mimeType;
|
|
269
|
+
return ref;
|
|
270
|
+
}
|
|
271
|
+
export {
|
|
272
|
+
apiFileRecordToCloudFile,
|
|
273
|
+
canonicalShareLinkToCloudShareLink,
|
|
274
|
+
cloudFileToMediaRef,
|
|
275
|
+
dbRowToCloudFile,
|
|
276
|
+
dbRowToCloudFilePermission,
|
|
277
|
+
dbRowToCloudFileVersion,
|
|
278
|
+
dbRowToCloudFolder,
|
|
279
|
+
fileIdToMediaRef,
|
|
280
|
+
parseCloudTreeRow,
|
|
281
|
+
parseCloudTreeRows,
|
|
282
|
+
rpcPermissionRowToCloudFilePermission,
|
|
283
|
+
toVisibility,
|
|
284
|
+
urlToMediaRef
|
|
285
|
+
};
|
|
286
|
+
//# sourceMappingURL=converters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/converters.ts"],"sourcesContent":["/**\n * features/files/redux/converters.ts\n *\n * Pure functions that translate DB rows (snake_case) and REST API shapes\n * (snake_case) into the domain types (camelCase) used throughout the app.\n *\n * No Redux, no Supabase client — these are pure and testable.\n */\n\nimport type {\n CloudFile,\n CloudFilePermission,\n CloudFilePermissionRow,\n CloudFileReadRow,\n CloudFileVersion,\n CloudFileVersionReadRow,\n CloudFolder,\n CloudFolderRow,\n CloudShareLink,\n CloudTreeRow,\n FileRecordApi,\n GranteeType,\n IamResourcePermissionRpcRow,\n MediaRef,\n PermissionLevel,\n ResourceType,\n Visibility,\n} from \"../types\";\nimport type { ShareLink as CanonicalShareLink } from \"../host/share-links\";\n// toVisibility / fileIdToMediaRef / urlToMediaRef moved to `@ai-matrx/media/files` (P16f).\nimport { toVisibility, fileIdToMediaRef, urlToMediaRef } from \"../../../files\";\nexport { toVisibility, fileIdToMediaRef, urlToMediaRef };\n\n// ---------------------------------------------------------------------------\n// Narrowing helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Narrow a DB / RPC visibility value to `Visibility`. This is a VALIDATION,\n * not a translation — the client speaks the same `platform.visibility`\n * vocabulary as the DB and the server, so every canonical label passes through\n * untouched.\n *\n * The legacy spellings are reconciled the same way the server does it\n * (`matrx_utils.visibility.LEGACY_VISIBILITY_MAP`): `shared` and `private`\n * both mean `personal`. They were retired from the enum on 2026-07-21 and\n * should no longer appear in any row; they are handled here only so an old\n * cached payload cannot land as an unknown value.\n *\n * EXPORTED on purpose: it is the remedy the visibility-vocabulary check names\n * for a legacy read (scripts/visibility-vocab/remedies.mjs) — one normalizer,\n * never a private copy per surface.\n */\n\nfunction toPermissionLevel(raw: string | null | undefined): PermissionLevel {\n return raw === \"write\" || raw === \"admin\" ? raw : \"read\";\n}\n\n/**\n * Canonical `public.permissions.permission_level` enum is `viewer < editor <\n * admin`. The file domain has always spoken `read < write < admin`. Translate\n * at the read boundary so the rest of the file feature is untouched.\n */\nfunction canonicalLevelToFileLevel(\n raw: string | null | undefined,\n): PermissionLevel {\n if (raw === \"admin\") return \"admin\";\n if (raw === \"editor\") return \"write\";\n return \"read\"; // 'viewer' and anything unexpected\n}\n\nfunction toResourceType(raw: string | null | undefined): ResourceType {\n return raw === \"folder\" ? \"folder\" : \"file\";\n}\n\nfunction toMetadataObject(raw: unknown): Record<string, unknown> {\n if (raw && typeof raw === \"object\" && !Array.isArray(raw)) {\n return raw as Record<string, unknown>;\n }\n return {};\n}\n\n// ---------------------------------------------------------------------------\n// Files\n// ---------------------------------------------------------------------------\n\nexport function dbRowToCloudFile(row: CloudFileReadRow): CloudFile {\n return {\n id: row.id,\n // `files.files` is canonical now: owner lives in `created_by` (trigger-\n // stamped), not the old `owner_id`. Domain keeps the `ownerId` field name.\n ownerId: row.created_by,\n organizationId: row.organization_id,\n filePath: row.file_path,\n fileName: row.file_name,\n mimeType: row.mime_type,\n // Phase 0 rename: `cld_files.file_size` → `cld_files.size_bytes`.\n // See docs/PYTHON_UPDATES.md §3. The Supabase generated row type\n // already carries the new column name.\n fileSize: row.size_bytes,\n checksum: row.checksum,\n visibility: toVisibility(row.visibility),\n currentVersion: row.current_version,\n parentFolderId: row.parent_folder_id,\n metadata: toMetadataObject(row.metadata),\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n // The DB has no computed-URL columns (public_url / url / cdn_url /\n // download_url are all server-computed on the REST path). Direct-DB\n // reads (Supabase realtime, SSR) leave these null; the resolver builds\n // the durable URL from the file id on demand via useFileSrc.\n publicUrl: null,\n url: null,\n cdnUrl: null,\n downloadUrl: null,\n // The DB no longer has a thumbnail_url column either (Phase 1b dropped\n // it); resolved server-side from the variants store on the REST path.\n // Direct-DB-read callers should fall back to `useFileAsset(fileId)`\n // and read `asset.variants[\"thumbnail_url\"].url`.\n thumbnailUrl: null,\n source: { kind: \"real\" },\n // Lineage + dimension columns exist on the DB row but were never mapped,\n // so realtime/SSR-sourced records lost derivedFrom + width/height that\n // REST-sourced records keep. Map them for shape parity.\n parentFileId: row.parent_file_id ?? null,\n derivationKind: row.derivation_kind ?? null,\n derivationMetadata:\n (row.derivation_metadata as Record<string, unknown> | null) ?? null,\n // Dedup-pyramid columns from Phase 2.0. Null on rows uploaded before\n // the dedup consolidation script ran or on rows that have never been\n // identified as duplicates / never had their canonical extract set.\n duplicateOfFileId: row.duplicate_of_file_id ?? null,\n canonicalProcessedDocumentId: row.canonical_processed_document_id ?? null,\n originDeviceId: row.origin_device_id ?? null,\n };\n}\n\n/**\n * Convert the REST API's FileRecord (snake_case) to the domain type. Shape\n * is identical to CloudFileRow modulo a few nullable defaults.\n */\nexport function apiFileRecordToCloudFile(row: FileRecordApi): CloudFile {\n // The Python FileRecord schema is gaining `duplicate_of_file_id` and\n // `canonical_processed_document_id` per the Phase 2.0 dedup handoff.\n // The OpenAPI types haven't been regenerated to include them yet, so\n // read defensively through a Record cast and treat them as optional.\n // Once the OpenAPI types ship the new fields, this cast becomes a\n // no-op and the values flow through unchanged.\n const extras = row as unknown as {\n organization_id?: string | null;\n duplicate_of_file_id?: string | null;\n canonical_processed_document_id?: string | null;\n parent_file_id?: string | null;\n derivation_kind?: string | null;\n derivation_metadata?: Record<string, unknown> | null;\n };\n return {\n id: row.id,\n ownerId: row.owner_id,\n organizationId: extras.organization_id ?? null,\n filePath: row.file_path,\n fileName: row.file_name,\n mimeType: row.mime_type ?? null,\n // Phase 0 rename: `FileRecord.file_size` → `FileRecord.size_bytes`.\n // The Python OpenAPI schema has been regenerated. See\n // docs/PYTHON_UPDATES.md §3.\n fileSize: row.size_bytes ?? null,\n checksum: row.checksum ?? null,\n visibility: toVisibility(row.visibility),\n currentVersion: row.current_version ?? 1,\n parentFolderId: row.parent_folder_id ?? null,\n metadata: toMetadataObject(row.metadata),\n createdAt: row.created_at ?? new Date().toISOString(),\n updatedAt: row.updated_at ?? new Date().toISOString(),\n deletedAt: row.deleted_at ?? null,\n // CDN URL for visibility=\"public\" files when the server has the CDN\n // feature enabled. Includes a ?v=<checksum[:8]> cache-buster.\n publicUrl: row.public_url ?? null,\n // The DURABLE URL envelope (api-types.ts FileRecord): `url` is the\n // canonical renderable locator (CDN for public, durable download route\n // for private), `cdn_url` public-only, `download_url` attachment\n // disposition. None of them expire.\n url: row.url ?? null,\n cdnUrl: row.cdn_url ?? null,\n downloadUrl: row.download_url ?? null,\n // Phase 1b: backend-rendered thumbnail (every file gets one). The\n // wire field is resolved server-side from the variants store now\n // that `cld_files.thumbnail_url` column has been dropped.\n thumbnailUrl: row.thumbnail_url ?? null,\n source: { kind: \"real\" },\n parentFileId: extras.parent_file_id ?? null,\n derivationKind: extras.derivation_kind ?? null,\n derivationMetadata: extras.derivation_metadata ?? null,\n duplicateOfFileId: extras.duplicate_of_file_id ?? null,\n canonicalProcessedDocumentId:\n extras.canonical_processed_document_id ?? null,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Folders\n// ---------------------------------------------------------------------------\n\nexport function dbRowToCloudFolder(row: CloudFolderRow): CloudFolder {\n return {\n id: row.id,\n // Canonical `files.folders`: owner is `created_by`, not `owner_id`.\n ownerId: row.created_by,\n folderPath: row.folder_path,\n folderName: row.folder_name,\n parentId: row.parent_id,\n visibility: toVisibility(row.visibility),\n metadata: toMetadataObject(row.metadata),\n createdAt: row.created_at,\n updatedAt: row.updated_at,\n deletedAt: row.deleted_at,\n source: { kind: \"real\" },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Versions\n// ---------------------------------------------------------------------------\n\nexport function dbRowToCloudFileVersion(\n row: CloudFileVersionReadRow,\n): CloudFileVersion {\n return {\n id: row.id,\n fileId: row.file_id,\n versionNumber: row.version_number,\n // Phase 0 rename — see `dbRowToCloudFile` above.\n fileSize: row.size_bytes,\n checksum: row.checksum,\n // Who saved THIS version is not recorded: files.file_versions has no\n // updated_by, and its created_by is rewritten to the FILE's owner\n // (component of files.files, db-rules §6d-1) — so it is unknown, never\n // the owner presented as the author.\n createdBy: null,\n createdAt: row.created_at,\n changeSummary: row.change_summary,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Permissions\n// ---------------------------------------------------------------------------\n\n/**\n * Map a canonical `iam.permissions` row (resource_type='file') into the file\n * domain's grant shape.\n *\n * `iam.permissions` distinguishes the grant target by which of three mutually-\n * exclusive columns is set — enforced by the CHECK `user_or_org_or_public`:\n * - user grant: `granted_to_user_id` set → `granteeType: \"user\"`, `granteeId` = user id\n * - org grant: `granted_to_organization_id`→ `granteeType: \"group\"`, `granteeId` = org id\n * - public grant: `is_public = true` (both FKs null) → `granteeType: \"public\"`\n *\n * A PUBLIC grant has no grantee id. We must NOT collapse it to a\n * `granteeType: \"user\"` grant with `granteeId: \"\"` — that silently mislabels\n * \"anyone with access\" as an empty user grant (member counts, avatar stacks,\n * and the user/group revoke path all then act on a bogus \"\" grantee). Instead\n * it is modeled distinctly (`granteeType: \"public\"`) and carries the row's own\n * `id` as `granteeId` so the value is real and unique (never `\"\"`). Downstream\n * member/avatar UIs exclude `\"public\"` grants; revoking one flips the\n * resource's visibility rather than deleting a by-grantee grant.\n *\n * Grant expiry: `iam.permissions.expires_at` (timestamptz, nullable) is the\n * canonical column — wire it straight through to the domain's `expiresAt`.\n */\nexport function dbRowToCloudFilePermission(\n row: CloudFilePermissionRow,\n): CloudFilePermission {\n const isPublicGrant =\n row.is_public === true &&\n row.granted_to_user_id == null &&\n row.granted_to_organization_id == null;\n const isOrgGrant = row.granted_to_organization_id != null;\n\n const granteeType: GranteeType = isPublicGrant\n ? \"public\"\n : isOrgGrant\n ? \"group\"\n : \"user\";\n\n // A public grant has no grantee FK; use the row id so `granteeId` is always\n // a real, unique value (never `\"\"`). User/org grants carry their FK id.\n const granteeId = isPublicGrant\n ? row.id\n : (row.granted_to_user_id ?? row.granted_to_organization_id ?? row.id);\n\n return {\n id: row.id,\n resourceId: row.resource_id,\n resourceType: toResourceType(row.resource_type),\n granteeId,\n granteeType,\n permissionLevel: canonicalLevelToFileLevel(row.permission_level),\n grantedBy: row.created_by,\n grantedAt: row.created_at ?? new Date().toISOString(),\n expiresAt: row.expires_at,\n };\n}\n\n/**\n * Map the summary JSON from `iam.fn_*_resource_permission` RPCs into the file\n * domain shape. The RPC omits the row `id` and canonical columns — callers\n * that need the full row should read `iam.permissions` directly (see\n * `loadPermissions`).\n */\nexport function rpcPermissionRowToCloudFilePermission(\n row: IamResourcePermissionRpcRow,\n): CloudFilePermission {\n const granteeType: GranteeType =\n row.grantee_type === \"organization\" ? \"group\" : \"user\";\n\n return {\n id: row.grantee_id,\n resourceId: row.resource_id,\n resourceType: toResourceType(row.resource_type),\n granteeId: row.grantee_id,\n granteeType,\n permissionLevel: toPermissionLevel(row.permission_level),\n grantedBy: row.granted_by,\n grantedAt: new Date().toISOString(),\n expiresAt: row.expires_at,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Share links\n// ---------------------------------------------------------------------------\n\n/**\n * Map a canonical share link (`utils/permissions/shareLinks.ts` `ShareLink`,\n * from the `list_share_links` / `create_share_link` RPC family over\n * `platform.share_links`) into the file domain shape. The RPCs don't echo the\n * resource identity, so the caller supplies it.\n */\nexport function canonicalShareLinkToCloudShareLink(\n link: CanonicalShareLink,\n resourceType: ResourceType,\n resourceId: string,\n): CloudShareLink {\n return {\n id: link.id,\n resourceId,\n resourceType,\n shareToken: link.token,\n // File links are minted viewer/editor; anything above editor still means\n // \"can modify\" for display purposes.\n permissionLevel: link.permissionLevel === \"viewer\" ? \"viewer\" : \"editor\",\n label: link.label,\n createdAt: link.createdAt,\n expiresAt: link.expiresAt,\n maxUses: link.maxUses,\n useCount: link.useCount,\n isActive: link.isActive,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Tree RPC — tolerant reader\n// ---------------------------------------------------------------------------\n//\n// The RPC returns `Json` (opaque). We shape-check defensively — if the Python\n// team updates the schema, only this function changes.\n\ntype LooseRow = Record<string, unknown>;\n\nfunction str(row: LooseRow, key: string): string | null {\n const v = row[key];\n return typeof v === \"string\" ? v : null;\n}\n\nfunction num(row: LooseRow, key: string): number | null {\n const v = row[key];\n return typeof v === \"number\" ? v : null;\n}\n\n/**\n * Parse one row from `cloud_get_user_file_tree`. Returns null if the row\n * doesn't have enough shape to use.\n *\n * Supports two shapes:\n * - Explicit `kind: 'file' | 'folder'` discriminator.\n * - Implicit: presence of `file_name` vs `folder_name`.\n */\nexport function parseCloudTreeRow(raw: unknown): CloudTreeRow | null {\n if (!raw || typeof raw !== \"object\") return null;\n const row = raw as LooseRow;\n\n const id = str(row, \"id\");\n if (!id) return null;\n\n // Owner identity drives RLS/permission decisions downstream\n // (`decideForOwnedFile` compares against the current user). The 2026\n // file-system canonicalization renamed the DB column `owner_id` →\n // `created_by`; `get_user_file_tree` now emits `created_by`. Read both\n // during the transition. A missing owner is a malformed row — defaulting\n // to \"\" would silently misclassify ownership rather than drop the row.\n const ownerId = str(row, \"owner_id\") ?? str(row, \"created_by\");\n if (!ownerId) return null;\n\n const explicitKind = str(row, \"kind\");\n const hasFolderName = typeof row.folder_name === \"string\";\n const hasFileName = typeof row.file_name === \"string\";\n\n const isFolder = explicitKind === \"folder\" || (hasFolderName && !hasFileName);\n\n const created = str(row, \"created_at\") ?? new Date().toISOString();\n const updated = str(row, \"updated_at\") ?? created;\n const visibility = toVisibility(str(row, \"visibility\") ?? \"personal\");\n const effPerm = str(row, \"effective_permission\");\n\n // ── Cross-shape readers ────────────────────────────────────────────────\n //\n // The RPC has TWO documented row shapes that we must both accept:\n //\n // Legacy (kind-specific fields):\n // { kind, id, owner_id,\n // file_path|folder_path, file_name|folder_name,\n // parent_folder_id|parent_id, mime_type, file_size, ... }\n //\n // Unified (Python team release notes — \"Returns folders too now.\n // Each row has a `kind` discriminator\"):\n // { kind, id, owner_id, path, name, parent_id,\n // mime_type, size_bytes, ... }\n //\n // Reading both keeps the FE working whether the deployed Postgres\n // function returns the old or the new shape — useful during rollouts\n // and in dev / staging where the migration order can be different\n // from prod. The output `CloudTreeRow` keeps the kind-specific names\n // so existing consumers don't need to change.\n //\n // Diagnosed 2026-04-26: the Python team's RPC update shipped the\n // unified shape; the FE was still reading the legacy field names,\n // which meant every row landed in `filesById` with empty\n // `filePath`/`fileName` and `undefined` `parentFolderId`. The Home\n // view rendered them as blank rows; the deeper folder filters\n // returned zero matches because `parentFolderId === activeFolderId`\n // failed (`undefined !== null`). Result: cloud-files appeared empty\n // even though every row was in state.\n // ────────────────────────────────────────────────────────────────────\n const folderPath = str(row, \"folder_path\") ?? str(row, \"path\") ?? \"\";\n const folderName = str(row, \"folder_name\") ?? str(row, \"name\") ?? \"\";\n const filePath = str(row, \"file_path\") ?? str(row, \"path\") ?? \"\";\n const fileName = str(row, \"file_name\") ?? str(row, \"name\") ?? \"\";\n const parentFolderId = str(row, \"parent_folder_id\") ?? str(row, \"parent_id\");\n // Phase 0 rename: `file_size` → `size_bytes`. Read both during the\n // transition; emit canonical `size_bytes` on the output row.\n const sizeBytes = num(row, \"size_bytes\") ?? num(row, \"file_size\");\n\n if (isFolder) {\n return {\n kind: \"folder\",\n id,\n folder_path: folderPath,\n folder_name: folderName,\n parent_id: str(row, \"parent_id\"),\n visibility,\n effective_permission: effPerm ? toPermissionLevel(effPerm) : null,\n owner_id: ownerId,\n created_at: created,\n updated_at: updated,\n deleted_at: str(row, \"deleted_at\"),\n metadata: toMetadataObject(row.metadata),\n };\n }\n\n return {\n kind: \"file\",\n id,\n file_path: filePath,\n file_name: fileName,\n parent_folder_id: parentFolderId,\n mime_type: str(row, \"mime_type\"),\n size_bytes: sizeBytes,\n visibility,\n current_version: num(row, \"current_version\") ?? 1,\n effective_permission: effPerm ? toPermissionLevel(effPerm) : null,\n owner_id: ownerId,\n created_at: created,\n updated_at: updated,\n deleted_at: str(row, \"deleted_at\"),\n origin_device_id: str(row, \"origin_device_id\"),\n metadata: toMetadataObject(row.metadata),\n };\n}\n\n/**\n * Parse the full RPC return (array of rows). Skips malformed rows rather than\n * throwing — a single bad row shouldn't blank the tree.\n */\nexport function parseCloudTreeRows(raw: unknown): CloudTreeRow[] {\n if (!Array.isArray(raw)) return [];\n const out: CloudTreeRow[] = [];\n for (const item of raw) {\n const parsed = parseCloudTreeRow(item);\n if (parsed) out.push(parsed);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// MediaRef builders — outbound API content blocks\n// ---------------------------------------------------------------------------\n//\n// Build a `MediaRef` (the canonical outbound-content reference shape; see\n// [features/files/types.ts](../types.ts)) from whatever the caller has on\n// hand. Always prefer `cloudFileToMediaRef` — it picks `file_id` (preferred)\n// and pulls the mime hint from the in-store record. Use `fileIdToMediaRef`\n// when you have the id but not the full record. Use `urlToMediaRef` only\n// for genuinely external URLs (e.g. a user pasting a public link).\n//\n// Why this lives here: every `addResource` callsite, every chat input, every\n// agent-runner attachment flows through one of these builders. Adding a new\n// identifier scheme (e.g. a `cdn_url` field) means editing this one file.\n\n/** Build a MediaRef from an in-store CloudFile. Prefers `file_id`. */\nexport function cloudFileToMediaRef(file: CloudFile): MediaRef {\n const ref: MediaRef = { file_id: file.id };\n if (file.mimeType) ref.mime_type = file.mimeType;\n return ref;\n}\n"],"mappings":"AA8BA,SAAS,cAAc,kBAAkB,qBAAqB;AAwB9D,SAAS,kBAAkB,KAAiD;AAC1E,SAAO,QAAQ,WAAW,QAAQ,UAAU,MAAM;AACpD;AAOA,SAAS,0BACP,KACiB;AACjB,MAAI,QAAQ,QAAS,QAAO;AAC5B,MAAI,QAAQ,SAAU,QAAO;AAC7B,SAAO;AACT;AAEA,SAAS,eAAe,KAA8C;AACpE,SAAO,QAAQ,WAAW,WAAW;AACvC;AAEA,SAAS,iBAAiB,KAAuC;AAC/D,MAAI,OAAO,OAAO,QAAQ,YAAY,CAAC,MAAM,QAAQ,GAAG,GAAG;AACzD,WAAO;AAAA,EACT;AACA,SAAO,CAAC;AACV;AAMO,SAAS,iBAAiB,KAAkC;AACjE,SAAO;AAAA,IACL,IAAI,IAAI;AAAA;AAAA;AAAA,IAGR,SAAS,IAAI;AAAA,IACb,gBAAgB,IAAI;AAAA,IACpB,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA;AAAA;AAAA;AAAA,IAId,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,YAAY,aAAa,IAAI,UAAU;AAAA,IACvC,gBAAgB,IAAI;AAAA,IACpB,gBAAgB,IAAI;AAAA,IACpB,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACvC,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,IAKf,WAAW;AAAA,IACX,KAAK;AAAA,IACL,QAAQ;AAAA,IACR,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA,IAKb,cAAc;AAAA,IACd,QAAQ,EAAE,MAAM,OAAO;AAAA;AAAA;AAAA;AAAA,IAIvB,cAAc,IAAI,kBAAkB;AAAA,IACpC,gBAAgB,IAAI,mBAAmB;AAAA,IACvC,oBACG,IAAI,uBAA0D;AAAA;AAAA;AAAA;AAAA,IAIjE,mBAAmB,IAAI,wBAAwB;AAAA,IAC/C,8BAA8B,IAAI,mCAAmC;AAAA,IACrE,gBAAgB,IAAI,oBAAoB;AAAA,EAC1C;AACF;AAMO,SAAS,yBAAyB,KAA+B;AAOtE,QAAM,SAAS;AAQf,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,SAAS,IAAI;AAAA,IACb,gBAAgB,OAAO,mBAAmB;AAAA,IAC1C,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA,IACd,UAAU,IAAI,aAAa;AAAA;AAAA;AAAA;AAAA,IAI3B,UAAU,IAAI,cAAc;AAAA,IAC5B,UAAU,IAAI,YAAY;AAAA,IAC1B,YAAY,aAAa,IAAI,UAAU;AAAA,IACvC,gBAAgB,IAAI,mBAAmB;AAAA,IACvC,gBAAgB,IAAI,oBAAoB;AAAA,IACxC,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACvC,WAAW,IAAI,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpD,WAAW,IAAI,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpD,WAAW,IAAI,cAAc;AAAA;AAAA;AAAA,IAG7B,WAAW,IAAI,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA,IAK7B,KAAK,IAAI,OAAO;AAAA,IAChB,QAAQ,IAAI,WAAW;AAAA,IACvB,aAAa,IAAI,gBAAgB;AAAA;AAAA;AAAA;AAAA,IAIjC,cAAc,IAAI,iBAAiB;AAAA,IACnC,QAAQ,EAAE,MAAM,OAAO;AAAA,IACvB,cAAc,OAAO,kBAAkB;AAAA,IACvC,gBAAgB,OAAO,mBAAmB;AAAA,IAC1C,oBAAoB,OAAO,uBAAuB;AAAA,IAClD,mBAAmB,OAAO,wBAAwB;AAAA,IAClD,8BACE,OAAO,mCAAmC;AAAA,EAC9C;AACF;AAMO,SAAS,mBAAmB,KAAkC;AACnE,SAAO;AAAA,IACL,IAAI,IAAI;AAAA;AAAA,IAER,SAAS,IAAI;AAAA,IACb,YAAY,IAAI;AAAA,IAChB,YAAY,IAAI;AAAA,IAChB,UAAU,IAAI;AAAA,IACd,YAAY,aAAa,IAAI,UAAU;AAAA,IACvC,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACvC,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,QAAQ,EAAE,MAAM,OAAO;AAAA,EACzB;AACF;AAMO,SAAS,wBACd,KACkB;AAClB,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,eAAe,IAAI;AAAA;AAAA,IAEnB,UAAU,IAAI;AAAA,IACd,UAAU,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,IAKd,WAAW;AAAA,IACX,WAAW,IAAI;AAAA,IACf,eAAe,IAAI;AAAA,EACrB;AACF;AA4BO,SAAS,2BACd,KACqB;AACrB,QAAM,gBACJ,IAAI,cAAc,QAClB,IAAI,sBAAsB,QAC1B,IAAI,8BAA8B;AACpC,QAAM,aAAa,IAAI,8BAA8B;AAErD,QAAM,cAA2B,gBAC7B,WACA,aACE,UACA;AAIN,QAAM,YAAY,gBACd,IAAI,KACH,IAAI,sBAAsB,IAAI,8BAA8B,IAAI;AAErE,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,YAAY,IAAI;AAAA,IAChB,cAAc,eAAe,IAAI,aAAa;AAAA,IAC9C;AAAA,IACA;AAAA,IACA,iBAAiB,0BAA0B,IAAI,gBAAgB;AAAA,IAC/D,WAAW,IAAI;AAAA,IACf,WAAW,IAAI,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpD,WAAW,IAAI;AAAA,EACjB;AACF;AAQO,SAAS,sCACd,KACqB;AACrB,QAAM,cACJ,IAAI,iBAAiB,iBAAiB,UAAU;AAElD,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,YAAY,IAAI;AAAA,IAChB,cAAc,eAAe,IAAI,aAAa;AAAA,IAC9C,WAAW,IAAI;AAAA,IACf;AAAA,IACA,iBAAiB,kBAAkB,IAAI,gBAAgB;AAAA,IACvD,WAAW,IAAI;AAAA,IACf,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,WAAW,IAAI;AAAA,EACjB;AACF;AAYO,SAAS,mCACd,MACA,cACA,YACgB;AAChB,SAAO;AAAA,IACL,IAAI,KAAK;AAAA,IACT;AAAA,IACA;AAAA,IACA,YAAY,KAAK;AAAA;AAAA;AAAA,IAGjB,iBAAiB,KAAK,oBAAoB,WAAW,WAAW;AAAA,IAChE,OAAO,KAAK;AAAA,IACZ,WAAW,KAAK;AAAA,IAChB,WAAW,KAAK;AAAA,IAChB,SAAS,KAAK;AAAA,IACd,UAAU,KAAK;AAAA,IACf,UAAU,KAAK;AAAA,EACjB;AACF;AAWA,SAAS,IAAI,KAAe,KAA4B;AACtD,QAAM,IAAI,IAAI,GAAG;AACjB,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;AAEA,SAAS,IAAI,KAAe,KAA4B;AACtD,QAAM,IAAI,IAAI,GAAG;AACjB,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;AAUO,SAAS,kBAAkB,KAAmC;AACnE,MAAI,CAAC,OAAO,OAAO,QAAQ,SAAU,QAAO;AAC5C,QAAM,MAAM;AAEZ,QAAM,KAAK,IAAI,KAAK,IAAI;AACxB,MAAI,CAAC,GAAI,QAAO;AAQhB,QAAM,UAAU,IAAI,KAAK,UAAU,KAAK,IAAI,KAAK,YAAY;AAC7D,MAAI,CAAC,QAAS,QAAO;AAErB,QAAM,eAAe,IAAI,KAAK,MAAM;AACpC,QAAM,gBAAgB,OAAO,IAAI,gBAAgB;AACjD,QAAM,cAAc,OAAO,IAAI,cAAc;AAE7C,QAAM,WAAW,iBAAiB,YAAa,iBAAiB,CAAC;AAEjE,QAAM,UAAU,IAAI,KAAK,YAAY,MAAK,oBAAI,KAAK,GAAE,YAAY;AACjE,QAAM,UAAU,IAAI,KAAK,YAAY,KAAK;AAC1C,QAAM,aAAa,aAAa,IAAI,KAAK,YAAY,KAAK,UAAU;AACpE,QAAM,UAAU,IAAI,KAAK,sBAAsB;AA+B/C,QAAM,aAAa,IAAI,KAAK,aAAa,KAAK,IAAI,KAAK,MAAM,KAAK;AAClE,QAAM,aAAa,IAAI,KAAK,aAAa,KAAK,IAAI,KAAK,MAAM,KAAK;AAClE,QAAM,WAAW,IAAI,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,KAAK;AAC9D,QAAM,WAAW,IAAI,KAAK,WAAW,KAAK,IAAI,KAAK,MAAM,KAAK;AAC9D,QAAM,iBAAiB,IAAI,KAAK,kBAAkB,KAAK,IAAI,KAAK,WAAW;AAG3E,QAAM,YAAY,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,WAAW;AAEhE,MAAI,UAAU;AACZ,WAAO;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,aAAa;AAAA,MACb,aAAa;AAAA,MACb,WAAW,IAAI,KAAK,WAAW;AAAA,MAC/B;AAAA,MACA,sBAAsB,UAAU,kBAAkB,OAAO,IAAI;AAAA,MAC7D,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ,YAAY,IAAI,KAAK,YAAY;AAAA,MACjC,UAAU,iBAAiB,IAAI,QAAQ;AAAA,IACzC;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA,WAAW;AAAA,IACX,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,WAAW,IAAI,KAAK,WAAW;AAAA,IAC/B,YAAY;AAAA,IACZ;AAAA,IACA,iBAAiB,IAAI,KAAK,iBAAiB,KAAK;AAAA,IAChD,sBAAsB,UAAU,kBAAkB,OAAO,IAAI;AAAA,IAC7D,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,YAAY;AAAA,IACZ,YAAY,IAAI,KAAK,YAAY;AAAA,IACjC,kBAAkB,IAAI,KAAK,kBAAkB;AAAA,IAC7C,UAAU,iBAAiB,IAAI,QAAQ;AAAA,EACzC;AACF;AAMO,SAAS,mBAAmB,KAA8B;AAC/D,MAAI,CAAC,MAAM,QAAQ,GAAG,EAAG,QAAO,CAAC;AACjC,QAAM,MAAsB,CAAC;AAC7B,aAAW,QAAQ,KAAK;AACtB,UAAM,SAAS,kBAAkB,IAAI;AACrC,QAAI,OAAQ,KAAI,KAAK,MAAM;AAAA,EAC7B;AACA,SAAO;AACT;AAkBO,SAAS,oBAAoB,MAA2B;AAC7D,QAAM,MAAgB,EAAE,SAAS,KAAK,GAAG;AACzC,MAAI,KAAK,SAAU,KAAI,YAAY,KAAK;AACxC,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical cloud-file field hydration contracts.
|
|
3
|
+
*
|
|
4
|
+
* A file id is the durable identity. Callers may seed fields they already
|
|
5
|
+
* possess, then request one of these field sets. Readiness is determined by
|
|
6
|
+
* `_loadedFields`, never by value truthiness: a loaded `mimeType: null` is a
|
|
7
|
+
* completed read and must not create a request loop.
|
|
8
|
+
*/
|
|
9
|
+
import type { CloudFile, CloudFileRecord, FileIdentityHint } from "../types.js";
|
|
10
|
+
export declare const FILE_RENDER_FIELDS: readonly ["fileName", "mimeType", "fileSize", "visibility"];
|
|
11
|
+
export declare const FILE_DB_RECORD_FIELDS: readonly ["ownerId", "filePath", "fileName", "mimeType", "fileSize", "checksum", "visibility", "currentVersion", "parentFolderId", "metadata", "createdAt", "updatedAt", "deletedAt", "parentFileId", "derivationKind", "derivationMetadata", "duplicateOfFileId", "canonicalProcessedDocumentId"];
|
|
12
|
+
export type CloudFileHydrationField = (typeof FILE_DB_RECORD_FIELDS)[number];
|
|
13
|
+
export declare function fileHintToCloudFilePartial(fileId: string, hint: FileIdentityHint | undefined): Partial<CloudFile> & Pick<CloudFile, "id">;
|
|
14
|
+
export declare function areCloudFileFieldsLoaded(record: Pick<CloudFileRecord, "_loadedFields"> | undefined, fields: readonly CloudFileHydrationField[]): boolean;
|
|
15
|
+
export declare function needsOnlyRenderFields(fields: readonly CloudFileHydrationField[]): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const FILE_RENDER_FIELDS = [
|
|
2
|
+
"fileName",
|
|
3
|
+
"mimeType",
|
|
4
|
+
"fileSize",
|
|
5
|
+
"visibility"
|
|
6
|
+
];
|
|
7
|
+
const FILE_DB_RECORD_FIELDS = [
|
|
8
|
+
"ownerId",
|
|
9
|
+
"filePath",
|
|
10
|
+
"fileName",
|
|
11
|
+
"mimeType",
|
|
12
|
+
"fileSize",
|
|
13
|
+
"checksum",
|
|
14
|
+
"visibility",
|
|
15
|
+
"currentVersion",
|
|
16
|
+
"parentFolderId",
|
|
17
|
+
"metadata",
|
|
18
|
+
"createdAt",
|
|
19
|
+
"updatedAt",
|
|
20
|
+
"deletedAt",
|
|
21
|
+
"parentFileId",
|
|
22
|
+
"derivationKind",
|
|
23
|
+
"derivationMetadata",
|
|
24
|
+
"duplicateOfFileId",
|
|
25
|
+
"canonicalProcessedDocumentId"
|
|
26
|
+
];
|
|
27
|
+
function fileHintToCloudFilePartial(fileId, hint) {
|
|
28
|
+
return hint ? { id: fileId, ...hint } : { id: fileId };
|
|
29
|
+
}
|
|
30
|
+
function areCloudFileFieldsLoaded(record, fields) {
|
|
31
|
+
if (!record) return false;
|
|
32
|
+
return fields.every((field) => record._loadedFields[field] === true);
|
|
33
|
+
}
|
|
34
|
+
function needsOnlyRenderFields(fields) {
|
|
35
|
+
return fields.every(
|
|
36
|
+
(field) => FILE_RENDER_FIELDS.includes(field)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
FILE_DB_RECORD_FIELDS,
|
|
41
|
+
FILE_RENDER_FIELDS,
|
|
42
|
+
areCloudFileFieldsLoaded,
|
|
43
|
+
fileHintToCloudFilePartial,
|
|
44
|
+
needsOnlyRenderFields
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=file-hydration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/file-hydration.ts"],"sourcesContent":["/**\n * Canonical cloud-file field hydration contracts.\n *\n * A file id is the durable identity. Callers may seed fields they already\n * possess, then request one of these field sets. Readiness is determined by\n * `_loadedFields`, never by value truthiness: a loaded `mimeType: null` is a\n * completed read and must not create a request loop.\n */\n\nimport type {\n CloudFile,\n CloudFileRecord,\n FileIdentityHint,\n} from \"../types\";\n\nexport const FILE_RENDER_FIELDS = [\n \"fileName\",\n \"mimeType\",\n \"fileSize\",\n \"visibility\",\n] as const satisfies readonly (keyof CloudFile)[];\n\nexport const FILE_DB_RECORD_FIELDS = [\n \"ownerId\",\n \"filePath\",\n \"fileName\",\n \"mimeType\",\n \"fileSize\",\n \"checksum\",\n \"visibility\",\n \"currentVersion\",\n \"parentFolderId\",\n \"metadata\",\n \"createdAt\",\n \"updatedAt\",\n \"deletedAt\",\n \"parentFileId\",\n \"derivationKind\",\n \"derivationMetadata\",\n \"duplicateOfFileId\",\n \"canonicalProcessedDocumentId\",\n] as const satisfies readonly (keyof CloudFile)[];\n\nexport type CloudFileHydrationField = (typeof FILE_DB_RECORD_FIELDS)[number];\n\nexport function fileHintToCloudFilePartial(\n fileId: string,\n hint: FileIdentityHint | undefined,\n): Partial<CloudFile> & Pick<CloudFile, \"id\"> {\n return hint ? { id: fileId, ...hint } : { id: fileId };\n}\n\nexport function areCloudFileFieldsLoaded(\n record: Pick<CloudFileRecord, \"_loadedFields\"> | undefined,\n fields: readonly CloudFileHydrationField[],\n): boolean {\n if (!record) return false;\n return fields.every((field) => record._loadedFields[field] === true);\n}\n\nexport function needsOnlyRenderFields(\n fields: readonly CloudFileHydrationField[],\n): boolean {\n return fields.every((field) =>\n (FILE_RENDER_FIELDS as readonly CloudFileHydrationField[]).includes(field),\n );\n}\n"],"mappings":"AAeO,MAAM,qBAAqB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,MAAM,wBAAwB;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,2BACd,QACA,MAC4C;AAC5C,SAAO,OAAO,EAAE,IAAI,QAAQ,GAAG,KAAK,IAAI,EAAE,IAAI,OAAO;AACvD;AAEO,SAAS,yBACd,QACA,QACS;AACT,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,OAAO,MAAM,CAAC,UAAU,OAAO,cAAc,KAAK,MAAM,IAAI;AACrE;AAEO,SAAS,sBACd,QACS;AACT,SAAO,OAAO;AAAA,IAAM,CAAC,UAClB,mBAA0D,SAAS,KAAK;AAAA,EAC3E;AACF;","names":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type AuthRetryableResult } from "../host/supabase.js";
|
|
2
|
+
/** Prevent a server-seeded Redux identity from issuing an RPC as anon. */
|
|
3
|
+
export declare function hasMatchingFileTreeSession(requestedUserId: string): Promise<boolean>;
|
|
4
|
+
/** Close the time-of-check/time-of-use gap around the authenticated RPC. */
|
|
5
|
+
export declare function runFileTreeSessionOperation<T>(run: () => PromiseLike<AuthRetryableResult<T>>): Promise<AuthRetryableResult<T>>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { supabase } from "../host/supabase.js";
|
|
2
|
+
import {
|
|
3
|
+
isMissingSessionError,
|
|
4
|
+
runWithSessionRetry,
|
|
5
|
+
SessionUnavailableError
|
|
6
|
+
} from "../host/supabase.js";
|
|
7
|
+
import { getClaimsUser } from "../host/supabase.js";
|
|
8
|
+
async function hasMatchingFileTreeSession(requestedUserId) {
|
|
9
|
+
const [{ data: sessionData }, { data: claims }] = await Promise.all([
|
|
10
|
+
supabase.auth.getSession(),
|
|
11
|
+
getClaimsUser(supabase)
|
|
12
|
+
]);
|
|
13
|
+
return Boolean(
|
|
14
|
+
sessionData.session?.access_token && claims.user?.id === requestedUserId
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
async function runFileTreeSessionOperation(run) {
|
|
18
|
+
const result = await runWithSessionRetry(run);
|
|
19
|
+
if (result.error && isMissingSessionError(result.error, result.status)) {
|
|
20
|
+
throw new SessionUnavailableError();
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
hasMatchingFileTreeSession,
|
|
26
|
+
runFileTreeSessionOperation
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=file-tree-auth-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/file-tree-auth-boundary.ts"],"sourcesContent":["import { supabase } from \"../host/supabase\";\nimport {\n isMissingSessionError,\n runWithSessionRetry,\n SessionUnavailableError,\n type AuthRetryableResult,\n} from \"../host/supabase\";\n\nimport { getClaimsUser } from \"../host/supabase\";\n/** Prevent a server-seeded Redux identity from issuing an RPC as anon. */\nexport async function hasMatchingFileTreeSession(\n requestedUserId: string,\n): Promise<boolean> {\n // `getSession()` is kept ONLY for the access-token string; WHO the caller is\n // comes from the token's locally verified claims, not from the cookie.\n const [{ data: sessionData }, { data: claims }] = await Promise.all([\n supabase.auth.getSession(),\n getClaimsUser(supabase),\n ]);\n\n return Boolean(\n sessionData.session?.access_token && claims.user?.id === requestedUserId,\n );\n}\n\n/** Close the time-of-check/time-of-use gap around the authenticated RPC. */\nexport async function runFileTreeSessionOperation<T>(\n run: () => PromiseLike<AuthRetryableResult<T>>,\n): Promise<AuthRetryableResult<T>> {\n const result = await runWithSessionRetry(run);\n if (result.error && isMissingSessionError(result.error, result.status)) {\n throw new SessionUnavailableError();\n }\n return result;\n}\n"],"mappings":"AAAA,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAEP,SAAS,qBAAqB;AAE9B,eAAsB,2BACpB,iBACkB;AAGlB,QAAM,CAAC,EAAE,MAAM,YAAY,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,MAAM,QAAQ,IAAI;AAAA,IAClE,SAAS,KAAK,WAAW;AAAA,IACzB,cAAc,QAAQ;AAAA,EACxB,CAAC;AAED,SAAO;AAAA,IACL,YAAY,SAAS,gBAAgB,OAAO,MAAM,OAAO;AAAA,EAC3D;AACF;AAGA,eAAsB,4BACpB,KACiC;AACjC,QAAM,SAAS,MAAM,oBAAoB,GAAG;AAC5C,MAAI,OAAO,SAAS,sBAAsB,OAAO,OAAO,OAAO,MAAM,GAAG;AACtE,UAAM,IAAI,wBAAwB;AAAA,EACpC;AACA,SAAO;AACT;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A gallery may wait for a real tree read, but it must never own an endless
|
|
3
|
+
* loading state. The shared producer uses this boundary so every tree
|
|
4
|
+
* consumer can retry after a stalled transport.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FILE_TREE_LOAD_TIMEOUT_MS = 20000;
|
|
7
|
+
export declare const FILE_TREE_LOAD_TIMEOUT_MESSAGE = "Your file library took too long to load. Try again.";
|
|
8
|
+
/**
|
|
9
|
+
* The timeout is a FAILURE the person felt, never a cancellation. A bare
|
|
10
|
+
* `controller.abort()` made postgrest report it as the stringified
|
|
11
|
+
* `"AbortError: signal is aborted without reason"` with an empty code — the
|
|
12
|
+
* same shape as a harmless cancel. Abort with a named `TimeoutError` reason and
|
|
13
|
+
* reject the thunk with it, so diagnostics can tell a stalled load from a cancel.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fileTreeLoadTimeoutError(): Error;
|
|
16
|
+
/**
|
|
17
|
+
* Still hidden-stalled after the retry: a deferred cancel, named AbortError so
|
|
18
|
+
* diagnostics treat it as one. The tree goes back to idle and reloads on use.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fileTreeLoadDeferredError(): Error;
|
|
21
|
+
export declare function isFileTreeLoadDeferred(error: unknown): boolean;
|
|
22
|
+
export declare function createFileTreeLoadTimeout(): {
|
|
23
|
+
controller: AbortController;
|
|
24
|
+
dispose: () => void;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Run one bounded tree load. A timeout while the tab is visible rejects with
|
|
28
|
+
* `TimeoutError` (a real failure, captured). A timeout while hidden waits for
|
|
29
|
+
* the tab to come back and retries once; hidden-stalled again, it rejects as a
|
|
30
|
+
* deferred cancel. Any other error from `attempt` propagates unchanged.
|
|
31
|
+
*/
|
|
32
|
+
export declare function runWithFileTreeLoadTimeout<T>(attempt: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDocumentHidden,
|
|
3
|
+
whenDocumentVisible
|
|
4
|
+
} from "../support/document-visibility.js";
|
|
5
|
+
const FILE_TREE_LOAD_TIMEOUT_MS = 2e4;
|
|
6
|
+
const FILE_TREE_LOAD_TIMEOUT_MESSAGE = "Your file library took too long to load. Try again.";
|
|
7
|
+
const HIDDEN_STALL_RETRIES = 1;
|
|
8
|
+
function fileTreeLoadTimeoutError() {
|
|
9
|
+
const error = new Error(FILE_TREE_LOAD_TIMEOUT_MESSAGE);
|
|
10
|
+
error.name = "TimeoutError";
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
const HIDDEN_STALL_NAME = "FileTreeHiddenStall";
|
|
14
|
+
function hiddenStall() {
|
|
15
|
+
const error = new Error(
|
|
16
|
+
"The file library load paused while the tab was hidden."
|
|
17
|
+
);
|
|
18
|
+
error.name = HIDDEN_STALL_NAME;
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
function fileTreeLoadDeferredError() {
|
|
22
|
+
const error = new Error(
|
|
23
|
+
"The file library load was deferred while the tab was hidden."
|
|
24
|
+
);
|
|
25
|
+
error.name = "AbortError";
|
|
26
|
+
return error;
|
|
27
|
+
}
|
|
28
|
+
function isFileTreeLoadDeferred(error) {
|
|
29
|
+
return error instanceof Error && error.name === "AbortError";
|
|
30
|
+
}
|
|
31
|
+
function createFileTreeLoadTimeout() {
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timeoutId = globalThis.setTimeout(
|
|
34
|
+
() => controller.abort(
|
|
35
|
+
isDocumentHidden() ? hiddenStall() : fileTreeLoadTimeoutError()
|
|
36
|
+
),
|
|
37
|
+
FILE_TREE_LOAD_TIMEOUT_MS
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
controller,
|
|
41
|
+
dispose: () => globalThis.clearTimeout(timeoutId)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function runWithFileTreeLoadTimeout(attempt) {
|
|
45
|
+
for (let hiddenRetries = 0; ; hiddenRetries += 1) {
|
|
46
|
+
const { controller, dispose } = createFileTreeLoadTimeout();
|
|
47
|
+
try {
|
|
48
|
+
return await attempt(controller.signal);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
if (!controller.signal.aborted) throw error;
|
|
51
|
+
const reason = controller.signal.reason;
|
|
52
|
+
if (!(reason instanceof Error) || reason.name !== HIDDEN_STALL_NAME) {
|
|
53
|
+
throw fileTreeLoadTimeoutError();
|
|
54
|
+
}
|
|
55
|
+
if (hiddenRetries >= HIDDEN_STALL_RETRIES) {
|
|
56
|
+
throw fileTreeLoadDeferredError();
|
|
57
|
+
}
|
|
58
|
+
} finally {
|
|
59
|
+
dispose();
|
|
60
|
+
}
|
|
61
|
+
await whenDocumentVisible();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
FILE_TREE_LOAD_TIMEOUT_MESSAGE,
|
|
66
|
+
FILE_TREE_LOAD_TIMEOUT_MS,
|
|
67
|
+
createFileTreeLoadTimeout,
|
|
68
|
+
fileTreeLoadDeferredError,
|
|
69
|
+
fileTreeLoadTimeoutError,
|
|
70
|
+
isFileTreeLoadDeferred,
|
|
71
|
+
runWithFileTreeLoadTimeout
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=file-tree-timeout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/files/engine/redux/file-tree-timeout.ts"],"sourcesContent":["import {\n isDocumentHidden,\n whenDocumentVisible,\n} from \"../support/document-visibility\";\n\n/**\n * A gallery may wait for a real tree read, but it must never own an endless\n * loading state. The shared producer uses this boundary so every tree\n * consumer can retry after a stalled transport.\n */\nexport const FILE_TREE_LOAD_TIMEOUT_MS = 20_000;\nexport const FILE_TREE_LOAD_TIMEOUT_MESSAGE =\n \"Your file library took too long to load. Try again.\";\n\n/** How many times a load that stalled in a hidden tab retries on return. */\nconst HIDDEN_STALL_RETRIES = 1;\n\n/**\n * The timeout is a FAILURE the person felt, never a cancellation. A bare\n * `controller.abort()` made postgrest report it as the stringified\n * `\"AbortError: signal is aborted without reason\"` with an empty code — the\n * same shape as a harmless cancel. Abort with a named `TimeoutError` reason and\n * reject the thunk with it, so diagnostics can tell a stalled load from a cancel.\n */\nexport function fileTreeLoadTimeoutError(): Error {\n const error = new Error(FILE_TREE_LOAD_TIMEOUT_MESSAGE);\n error.name = \"TimeoutError\";\n return error;\n}\n\nconst HIDDEN_STALL_NAME = \"FileTreeHiddenStall\";\n\n/**\n * A load that ran out of time while the tab was HIDDEN. The browser freezes\n * background network, so nobody felt this; it is retried when the person\n * returns (class sec_d904494698f4: a 7h hidden tab reported as an error).\n */\nfunction hiddenStall(): Error {\n const error = new Error(\n \"The file library load paused while the tab was hidden.\",\n );\n error.name = HIDDEN_STALL_NAME;\n return error;\n}\n\n/**\n * Still hidden-stalled after the retry: a deferred cancel, named AbortError so\n * diagnostics treat it as one. The tree goes back to idle and reloads on use.\n */\nexport function fileTreeLoadDeferredError(): Error {\n const error = new Error(\n \"The file library load was deferred while the tab was hidden.\",\n );\n error.name = \"AbortError\";\n return error;\n}\n\nexport function isFileTreeLoadDeferred(error: unknown): boolean {\n return error instanceof Error && error.name === \"AbortError\";\n}\n\nexport function createFileTreeLoadTimeout(): {\n controller: AbortController;\n dispose: () => void;\n} {\n const controller = new AbortController();\n const timeoutId = globalThis.setTimeout(\n () =>\n controller.abort(\n isDocumentHidden() ? hiddenStall() : fileTreeLoadTimeoutError(),\n ),\n FILE_TREE_LOAD_TIMEOUT_MS,\n );\n\n return {\n controller,\n dispose: () => globalThis.clearTimeout(timeoutId),\n };\n}\n\n/**\n * Run one bounded tree load. A timeout while the tab is visible rejects with\n * `TimeoutError` (a real failure, captured). A timeout while hidden waits for\n * the tab to come back and retries once; hidden-stalled again, it rejects as a\n * deferred cancel. Any other error from `attempt` propagates unchanged.\n */\nexport async function runWithFileTreeLoadTimeout<T>(\n attempt: (signal: AbortSignal) => Promise<T>,\n): Promise<T> {\n for (let hiddenRetries = 0; ; hiddenRetries += 1) {\n const { controller, dispose } = createFileTreeLoadTimeout();\n try {\n return await attempt(controller.signal);\n } catch (error) {\n if (!controller.signal.aborted) throw error;\n const reason: unknown = controller.signal.reason;\n if (!(reason instanceof Error) || reason.name !== HIDDEN_STALL_NAME) {\n throw fileTreeLoadTimeoutError();\n }\n if (hiddenRetries >= HIDDEN_STALL_RETRIES) {\n throw fileTreeLoadDeferredError();\n }\n } finally {\n dispose();\n }\n await whenDocumentVisible();\n }\n}\n"],"mappings":"AAAA;AAAA,EACE;AAAA,EACA;AAAA,OACK;AAOA,MAAM,4BAA4B;AAClC,MAAM,iCACX;AAGF,MAAM,uBAAuB;AAStB,SAAS,2BAAkC;AAChD,QAAM,QAAQ,IAAI,MAAM,8BAA8B;AACtD,QAAM,OAAO;AACb,SAAO;AACT;AAEA,MAAM,oBAAoB;AAO1B,SAAS,cAAqB;AAC5B,QAAM,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,QAAM,OAAO;AACb,SAAO;AACT;AAMO,SAAS,4BAAmC;AACjD,QAAM,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,QAAM,OAAO;AACb,SAAO;AACT;AAEO,SAAS,uBAAuB,OAAyB;AAC9D,SAAO,iBAAiB,SAAS,MAAM,SAAS;AAClD;AAEO,SAAS,4BAGd;AACA,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,YAAY,WAAW;AAAA,IAC3B,MACE,WAAW;AAAA,MACT,iBAAiB,IAAI,YAAY,IAAI,yBAAyB;AAAA,IAChE;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,SAAS,MAAM,WAAW,aAAa,SAAS;AAAA,EAClD;AACF;AAQA,eAAsB,2BACpB,SACY;AACZ,WAAS,gBAAgB,KAAK,iBAAiB,GAAG;AAChD,UAAM,EAAE,YAAY,QAAQ,IAAI,0BAA0B;AAC1D,QAAI;AACF,aAAO,MAAM,QAAQ,WAAW,MAAM;AAAA,IACxC,SAAS,OAAO;AACd,UAAI,CAAC,WAAW,OAAO,QAAS,OAAM;AACtC,YAAM,SAAkB,WAAW,OAAO;AAC1C,UAAI,EAAE,kBAAkB,UAAU,OAAO,SAAS,mBAAmB;AACnE,cAAM,yBAAyB;AAAA,MACjC;AACA,UAAI,iBAAiB,sBAAsB;AACzC,cAAM,0BAA0B;AAAA,MAClC;AAAA,IACF,UAAE;AACA,cAAQ;AAAA,IACV;AACA,UAAM,oBAAoB;AAAA,EAC5B;AACF;","names":[]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* features/files/redux/mutation-toast-middleware.ts
|
|
3
|
+
*
|
|
4
|
+
* Centralised user-facing error feedback for Cloud Files mutations (P0-4).
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ───────────────
|
|
8
|
+
* Every file/folder mutation thunk already rolls back its optimistic update
|
|
9
|
+
* and rethrows on failure — but the ~15 call sites (FileContextMenu,
|
|
10
|
+
* BulkActionsBar, useFileActions, the canonical mutation hooks, …) `.unwrap()`
|
|
11
|
+
* without a catch, so failures were SILENT: a rename that 403s just snapped
|
|
12
|
+
* back with no explanation; a delete that failed left no trace. In an
|
|
13
|
+
* enterprise file manager that is the single biggest "feels broken" driver.
|
|
14
|
+
*
|
|
15
|
+
* Rather than touch every call site (and every thunk's catch/finally shape),
|
|
16
|
+
* we listen once, here, for the `<prefix>/rejected` action that RTK's
|
|
17
|
+
* `createAsyncThunk` ALWAYS dispatches when a thunk throws — even when the
|
|
18
|
+
* caller's `.unwrap()` rethrows. One middleware → 100% coverage, no thunk
|
|
19
|
+
* edits, UI concern kept out of the thunk bodies.
|
|
20
|
+
*
|
|
21
|
+
* Reads (loadUserFileTree, loadPermissions, …) are intentionally excluded —
|
|
22
|
+
* we only shout about MUTATIONS the user just attempted. Aborted/conditioned
|
|
23
|
+
* dispatches (RTK `meta.aborted` / `meta.condition`) are skipped so a
|
|
24
|
+
* superseded request never toasts.
|
|
25
|
+
*/
|
|
26
|
+
import type { Middleware } from "@reduxjs/toolkit";
|
|
27
|
+
export declare const cloudFilesMutationToastMiddleware: Middleware;
|