@collage-dam/mcp-server 0.1.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/.env.example +56 -0
- package/CHANGELOG.md +90 -0
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/dist/client.d.ts +497 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1162 -0
- package/dist/client.js.map +1 -0
- package/dist/conventions/confirmation.d.ts +89 -0
- package/dist/conventions/confirmation.d.ts.map +1 -0
- package/dist/conventions/confirmation.js +132 -0
- package/dist/conventions/confirmation.js.map +1 -0
- package/dist/conventions/dry-run/batch-executor.d.ts +36 -0
- package/dist/conventions/dry-run/batch-executor.d.ts.map +1 -0
- package/dist/conventions/dry-run/batch-executor.js +89 -0
- package/dist/conventions/dry-run/batch-executor.js.map +1 -0
- package/dist/conventions/dry-run/diff-renderer.d.ts +34 -0
- package/dist/conventions/dry-run/diff-renderer.d.ts.map +1 -0
- package/dist/conventions/dry-run/diff-renderer.js +158 -0
- package/dist/conventions/dry-run/diff-renderer.js.map +1 -0
- package/dist/conventions/dry-run/index.d.ts +13 -0
- package/dist/conventions/dry-run/index.d.ts.map +1 -0
- package/dist/conventions/dry-run/index.js +10 -0
- package/dist/conventions/dry-run/index.js.map +1 -0
- package/dist/conventions/dry-run/mutating-tool.d.ts +64 -0
- package/dist/conventions/dry-run/mutating-tool.d.ts.map +1 -0
- package/dist/conventions/dry-run/mutating-tool.js +88 -0
- package/dist/conventions/dry-run/mutating-tool.js.map +1 -0
- package/dist/conventions/dry-run/summary.d.ts +66 -0
- package/dist/conventions/dry-run/summary.d.ts.map +1 -0
- package/dist/conventions/dry-run/summary.js +185 -0
- package/dist/conventions/dry-run/summary.js.map +1 -0
- package/dist/conventions/dry-run/types.d.ts +597 -0
- package/dist/conventions/dry-run/types.d.ts.map +1 -0
- package/dist/conventions/dry-run/types.js +108 -0
- package/dist/conventions/dry-run/types.js.map +1 -0
- package/dist/conventions/dry-run/with-dry-run.d.ts +66 -0
- package/dist/conventions/dry-run/with-dry-run.d.ts.map +1 -0
- package/dist/conventions/dry-run/with-dry-run.js +219 -0
- package/dist/conventions/dry-run/with-dry-run.js.map +1 -0
- package/dist/conventions/env.d.ts +49 -0
- package/dist/conventions/env.d.ts.map +1 -0
- package/dist/conventions/env.js +84 -0
- package/dist/conventions/env.js.map +1 -0
- package/dist/conventions/errors.d.ts +68 -0
- package/dist/conventions/errors.d.ts.map +1 -0
- package/dist/conventions/errors.js +81 -0
- package/dist/conventions/errors.js.map +1 -0
- package/dist/conventions/logger.d.ts +28 -0
- package/dist/conventions/logger.d.ts.map +1 -0
- package/dist/conventions/logger.js +105 -0
- package/dist/conventions/logger.js.map +1 -0
- package/dist/conventions/pagination.d.ts +37 -0
- package/dist/conventions/pagination.d.ts.map +1 -0
- package/dist/conventions/pagination.js +53 -0
- package/dist/conventions/pagination.js.map +1 -0
- package/dist/conventions/rate-limiter.d.ts +54 -0
- package/dist/conventions/rate-limiter.d.ts.map +1 -0
- package/dist/conventions/rate-limiter.js +143 -0
- package/dist/conventions/rate-limiter.js.map +1 -0
- package/dist/conventions/response-budget.d.ts +66 -0
- package/dist/conventions/response-budget.d.ts.map +1 -0
- package/dist/conventions/response-budget.js +89 -0
- package/dist/conventions/response-budget.js.map +1 -0
- package/dist/conventions/schema-version.d.ts +27 -0
- package/dist/conventions/schema-version.d.ts.map +1 -0
- package/dist/conventions/schema-version.js +29 -0
- package/dist/conventions/schema-version.js.map +1 -0
- package/dist/conventions/state-store-redis.d.ts +32 -0
- package/dist/conventions/state-store-redis.d.ts.map +1 -0
- package/dist/conventions/state-store-redis.js +77 -0
- package/dist/conventions/state-store-redis.js.map +1 -0
- package/dist/conventions/state-store.d.ts +46 -0
- package/dist/conventions/state-store.d.ts.map +1 -0
- package/dist/conventions/state-store.js +105 -0
- package/dist/conventions/state-store.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +421 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/collection-audit.d.ts +13 -0
- package/dist/prompts/collection-audit.d.ts.map +1 -0
- package/dist/prompts/collection-audit.js +168 -0
- package/dist/prompts/collection-audit.js.map +1 -0
- package/dist/prompts/create-distribution.d.ts +15 -0
- package/dist/prompts/create-distribution.d.ts.map +1 -0
- package/dist/prompts/create-distribution.js +111 -0
- package/dist/prompts/create-distribution.js.map +1 -0
- package/dist/prompts/helpers.d.ts +20 -0
- package/dist/prompts/helpers.d.ts.map +1 -0
- package/dist/prompts/helpers.js +53 -0
- package/dist/prompts/helpers.js.map +1 -0
- package/dist/prompts/library-health-audit.d.ts +13 -0
- package/dist/prompts/library-health-audit.d.ts.map +1 -0
- package/dist/prompts/library-health-audit.js +131 -0
- package/dist/prompts/library-health-audit.js.map +1 -0
- package/dist/prompts/usage-insights.d.ts +13 -0
- package/dist/prompts/usage-insights.d.ts.map +1 -0
- package/dist/prompts/usage-insights.js +98 -0
- package/dist/prompts/usage-insights.js.map +1 -0
- package/dist/prompts/wrap-prompt-as-tool.d.ts +48 -0
- package/dist/prompts/wrap-prompt-as-tool.d.ts.map +1 -0
- package/dist/prompts/wrap-prompt-as-tool.js +61 -0
- package/dist/prompts/wrap-prompt-as-tool.js.map +1 -0
- package/dist/resources/asset-by-id.d.ts +4 -0
- package/dist/resources/asset-by-id.d.ts.map +1 -0
- package/dist/resources/asset-by-id.js +27 -0
- package/dist/resources/asset-by-id.js.map +1 -0
- package/dist/resources/collections.d.ts +5 -0
- package/dist/resources/collections.d.ts.map +1 -0
- package/dist/resources/collections.js +48 -0
- package/dist/resources/collections.js.map +1 -0
- package/dist/resources/custom-fields.d.ts +4 -0
- package/dist/resources/custom-fields.d.ts.map +1 -0
- package/dist/resources/custom-fields.js +30 -0
- package/dist/resources/custom-fields.js.map +1 -0
- package/dist/resources/folders.d.ts +5 -0
- package/dist/resources/folders.d.ts.map +1 -0
- package/dist/resources/folders.js +73 -0
- package/dist/resources/folders.js.map +1 -0
- package/dist/resources/helpers.d.ts +17 -0
- package/dist/resources/helpers.d.ts.map +1 -0
- package/dist/resources/helpers.js +59 -0
- package/dist/resources/helpers.js.map +1 -0
- package/dist/resources/portals.d.ts +5 -0
- package/dist/resources/portals.d.ts.map +1 -0
- package/dist/resources/portals.js +81 -0
- package/dist/resources/portals.js.map +1 -0
- package/dist/resources/recent-and-dashboard.d.ts +5 -0
- package/dist/resources/recent-and-dashboard.d.ts.map +1 -0
- package/dist/resources/recent-and-dashboard.js +42 -0
- package/dist/resources/recent-and-dashboard.js.map +1 -0
- package/dist/tools/asset-selection.d.ts +102 -0
- package/dist/tools/asset-selection.d.ts.map +1 -0
- package/dist/tools/asset-selection.js +133 -0
- package/dist/tools/asset-selection.js.map +1 -0
- package/dist/tools/audit/audit-folder-structure.d.ts +108 -0
- package/dist/tools/audit/audit-folder-structure.d.ts.map +1 -0
- package/dist/tools/audit/audit-folder-structure.js +260 -0
- package/dist/tools/audit/audit-folder-structure.js.map +1 -0
- package/dist/tools/audit/audit-naming-conventions.d.ts +83 -0
- package/dist/tools/audit/audit-naming-conventions.d.ts.map +1 -0
- package/dist/tools/audit/audit-naming-conventions.js +238 -0
- package/dist/tools/audit/audit-naming-conventions.js.map +1 -0
- package/dist/tools/audit/audit-tagging-hygiene.d.ts +77 -0
- package/dist/tools/audit/audit-tagging-hygiene.d.ts.map +1 -0
- package/dist/tools/audit/audit-tagging-hygiene.js +402 -0
- package/dist/tools/audit/audit-tagging-hygiene.js.map +1 -0
- package/dist/tools/audit/detect-duplicates.d.ts +62 -0
- package/dist/tools/audit/detect-duplicates.d.ts.map +1 -0
- package/dist/tools/audit/detect-duplicates.js +0 -0
- package/dist/tools/audit/detect-duplicates.js.map +1 -0
- package/dist/tools/audit/types.d.ts +526 -0
- package/dist/tools/audit/types.d.ts.map +1 -0
- package/dist/tools/audit/types.js +188 -0
- package/dist/tools/audit/types.js.map +1 -0
- package/dist/tools/bulk-move-assets.d.ts +78 -0
- package/dist/tools/bulk-move-assets.d.ts.map +1 -0
- package/dist/tools/bulk-move-assets.js +122 -0
- package/dist/tools/bulk-move-assets.js.map +1 -0
- package/dist/tools/bulk-normalize-filenames.d.ts +62 -0
- package/dist/tools/bulk-normalize-filenames.d.ts.map +1 -0
- package/dist/tools/bulk-normalize-filenames.js +237 -0
- package/dist/tools/bulk-normalize-filenames.js.map +1 -0
- package/dist/tools/bulk-rename-assets.d.ts +79 -0
- package/dist/tools/bulk-rename-assets.d.ts.map +1 -0
- package/dist/tools/bulk-rename-assets.js +139 -0
- package/dist/tools/bulk-rename-assets.js.map +1 -0
- package/dist/tools/bulk-tags.d.ts +107 -0
- package/dist/tools/bulk-tags.d.ts.map +1 -0
- package/dist/tools/bulk-tags.js +220 -0
- package/dist/tools/bulk-tags.js.map +1 -0
- package/dist/tools/client-adapters.d.ts +76 -0
- package/dist/tools/client-adapters.d.ts.map +1 -0
- package/dist/tools/client-adapters.js +648 -0
- package/dist/tools/client-adapters.js.map +1 -0
- package/dist/tools/collection-membership.d.ts +90 -0
- package/dist/tools/collection-membership.d.ts.map +1 -0
- package/dist/tools/collection-membership.js +195 -0
- package/dist/tools/collection-membership.js.map +1 -0
- package/dist/tools/create-collection.d.ts +63 -0
- package/dist/tools/create-collection.d.ts.map +1 -0
- package/dist/tools/create-collection.js +151 -0
- package/dist/tools/create-collection.js.map +1 -0
- package/dist/tools/create-folder.d.ts +46 -0
- package/dist/tools/create-folder.d.ts.map +1 -0
- package/dist/tools/create-folder.js +83 -0
- package/dist/tools/create-folder.js.map +1 -0
- package/dist/tools/create-share-link.d.ts +107 -0
- package/dist/tools/create-share-link.d.ts.map +1 -0
- package/dist/tools/create-share-link.js +239 -0
- package/dist/tools/create-share-link.js.map +1 -0
- package/dist/tools/get-asset-details.d.ts +401 -0
- package/dist/tools/get-asset-details.d.ts.map +1 -0
- package/dist/tools/get-asset-details.js +56 -0
- package/dist/tools/get-asset-details.js.map +1 -0
- package/dist/tools/get-collection.d.ts +126 -0
- package/dist/tools/get-collection.d.ts.map +1 -0
- package/dist/tools/get-collection.js +52 -0
- package/dist/tools/get-collection.js.map +1 -0
- package/dist/tools/get-embed-code.d.ts +195 -0
- package/dist/tools/get-embed-code.d.ts.map +1 -0
- package/dist/tools/get-embed-code.js +214 -0
- package/dist/tools/get-embed-code.js.map +1 -0
- package/dist/tools/insights/analyze-share-links.d.ts +159 -0
- package/dist/tools/insights/analyze-share-links.d.ts.map +1 -0
- package/dist/tools/insights/analyze-share-links.js +314 -0
- package/dist/tools/insights/analyze-share-links.js.map +1 -0
- package/dist/tools/insights/insight-cache.d.ts +36 -0
- package/dist/tools/insights/insight-cache.d.ts.map +1 -0
- package/dist/tools/insights/insight-cache.js +98 -0
- package/dist/tools/insights/insight-cache.js.map +1 -0
- package/dist/tools/insights/report-asset-activation.d.ts +149 -0
- package/dist/tools/insights/report-asset-activation.d.ts.map +1 -0
- package/dist/tools/insights/report-asset-activation.js +380 -0
- package/dist/tools/insights/report-asset-activation.js.map +1 -0
- package/dist/tools/insights/report-stale-assets.d.ts +120 -0
- package/dist/tools/insights/report-stale-assets.d.ts.map +1 -0
- package/dist/tools/insights/report-stale-assets.js +281 -0
- package/dist/tools/insights/report-stale-assets.js.map +1 -0
- package/dist/tools/insights/report-top-assets.d.ts +139 -0
- package/dist/tools/insights/report-top-assets.d.ts.map +1 -0
- package/dist/tools/insights/report-top-assets.js +407 -0
- package/dist/tools/insights/report-top-assets.js.map +1 -0
- package/dist/tools/list-categories.d.ts +127 -0
- package/dist/tools/list-categories.d.ts.map +1 -0
- package/dist/tools/list-categories.js +68 -0
- package/dist/tools/list-categories.js.map +1 -0
- package/dist/tools/list-collections.d.ts +127 -0
- package/dist/tools/list-collections.d.ts.map +1 -0
- package/dist/tools/list-collections.js +53 -0
- package/dist/tools/list-collections.js.map +1 -0
- package/dist/tools/list-custom-fields.d.ts +125 -0
- package/dist/tools/list-custom-fields.d.ts.map +1 -0
- package/dist/tools/list-custom-fields.js +51 -0
- package/dist/tools/list-custom-fields.js.map +1 -0
- package/dist/tools/list-share-links.d.ts +192 -0
- package/dist/tools/list-share-links.d.ts.map +1 -0
- package/dist/tools/list-share-links.js +92 -0
- package/dist/tools/list-share-links.js.map +1 -0
- package/dist/tools/list-workspaces.d.ts +88 -0
- package/dist/tools/list-workspaces.d.ts.map +1 -0
- package/dist/tools/list-workspaces.js +71 -0
- package/dist/tools/list-workspaces.js.map +1 -0
- package/dist/tools/move-asset.d.ts +48 -0
- package/dist/tools/move-asset.d.ts.map +1 -0
- package/dist/tools/move-asset.js +85 -0
- package/dist/tools/move-asset.js.map +1 -0
- package/dist/tools/rename-asset.d.ts +88 -0
- package/dist/tools/rename-asset.d.ts.map +1 -0
- package/dist/tools/rename-asset.js +100 -0
- package/dist/tools/rename-asset.js.map +1 -0
- package/dist/tools/rename-folder.d.ts +55 -0
- package/dist/tools/rename-folder.d.ts.map +1 -0
- package/dist/tools/rename-folder.js +101 -0
- package/dist/tools/rename-folder.js.map +1 -0
- package/dist/tools/revoke-share-link.d.ts +55 -0
- package/dist/tools/revoke-share-link.d.ts.map +1 -0
- package/dist/tools/revoke-share-link.js +77 -0
- package/dist/tools/revoke-share-link.js.map +1 -0
- package/dist/tools/search/facets.d.ts +34 -0
- package/dist/tools/search/facets.d.ts.map +1 -0
- package/dist/tools/search/facets.js +147 -0
- package/dist/tools/search/facets.js.map +1 -0
- package/dist/tools/search/filter-builder.d.ts +33 -0
- package/dist/tools/search/filter-builder.d.ts.map +1 -0
- package/dist/tools/search/filter-builder.js +111 -0
- package/dist/tools/search/filter-builder.js.map +1 -0
- package/dist/tools/search/search-assets.d.ts +41 -0
- package/dist/tools/search/search-assets.d.ts.map +1 -0
- package/dist/tools/search/search-assets.js +162 -0
- package/dist/tools/search/search-assets.js.map +1 -0
- package/dist/tools/search/search-collections.d.ts +35 -0
- package/dist/tools/search/search-collections.d.ts.map +1 -0
- package/dist/tools/search/search-collections.js +103 -0
- package/dist/tools/search/search-collections.js.map +1 -0
- package/dist/tools/search/types.d.ts +1047 -0
- package/dist/tools/search/types.d.ts.map +1 -0
- package/dist/tools/search/types.js +216 -0
- package/dist/tools/search/types.js.map +1 -0
- package/dist/tools/update-asset-metadata.d.ts +78 -0
- package/dist/tools/update-asset-metadata.d.ts.map +1 -0
- package/dist/tools/update-asset-metadata.js +203 -0
- package/dist/tools/update-asset-metadata.js.map +1 -0
- package/dist/tools/update-collection.d.ts +69 -0
- package/dist/tools/update-collection.d.ts.map +1 -0
- package/dist/tools/update-collection.js +142 -0
- package/dist/tools/update-collection.js.map +1 -0
- package/dist/tools/view-category-contents.d.ts +231 -0
- package/dist/tools/view-category-contents.d.ts.map +1 -0
- package/dist/tools/view-category-contents.js +97 -0
- package/dist/tools/view-category-contents.js.map +1 -0
- package/dist/types.d.ts +1326 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +288 -0
- package/dist/types.js.map +1 -0
- package/dist/typesense.d.ts +84 -0
- package/dist/typesense.d.ts.map +1 -0
- package/dist/typesense.js +243 -0
- package/dist/typesense.js.map +1 -0
- package/docs/api-field-verification.md +244 -0
- package/docs/deployment-runbook.md +446 -0
- package/docs/security-review.md +195 -0
- package/docs/typesense-filter-schema.md +262 -0
- package/docs/verified-endpoints.md +38 -0
- package/package.json +72 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
// ── Tool Client Adapters ─────────────────────────────────────────────
|
|
2
|
+
// Each Phase 1 mutating tool declares a narrow client interface it needs
|
|
3
|
+
// (rename, metadata read/write, tag read/write, collection metadata
|
|
4
|
+
// read/write, single-collection membership read). This module wires those
|
|
5
|
+
// interfaces to live `CollageClient` methods.
|
|
6
|
+
//
|
|
7
|
+
// Read paths reuse `getAssetDetails` (POST view-detail) where possible so
|
|
8
|
+
// we avoid duplicating round-trips for the same data. Custom-field reads
|
|
9
|
+
// fan out to `getAssetCustomFields` because `view-detail` does not include
|
|
10
|
+
// the custom-field projection in the same shape the metadata tool needs.
|
|
11
|
+
//
|
|
12
|
+
// The lone exception is `searchAssets` (Typesense-backed) — that adapter
|
|
13
|
+
// stays UPSTREAM-stubbed until the public Typesense host is provisioned.
|
|
14
|
+
import { createToolError } from '../conventions/errors.js';
|
|
15
|
+
import { generateRequestId } from '../conventions/logger.js';
|
|
16
|
+
import { runSearchAssets } from './search/search-assets.js';
|
|
17
|
+
import { ENUMERATOR_HARD_CAP, } from './insights/analyze-share-links.js';
|
|
18
|
+
// ── Asset Enumerator (audit_tagging_hygiene) ─────────────────────────
|
|
19
|
+
/**
|
|
20
|
+
* Backed by `CollageClient.enumerateAssets()` which iterates folders via
|
|
21
|
+
* `view-files-with-category`. Workspace-wide flat enumeration is otherwise
|
|
22
|
+
* Typesense-only.
|
|
23
|
+
*
|
|
24
|
+
* Returns a typed `UPSTREAM` error if folder listing 402s (current
|
|
25
|
+
* BREZ-workspace state) — the audit tool surfaces that to the caller as
|
|
26
|
+
* a structured failure instead of an empty result.
|
|
27
|
+
*/
|
|
28
|
+
export function buildAssetEnumerator(client) {
|
|
29
|
+
return {
|
|
30
|
+
enumerateAssets: async () => {
|
|
31
|
+
const res = await client.enumerateAssets();
|
|
32
|
+
if (!res.ok)
|
|
33
|
+
return { ok: false, error: res.error };
|
|
34
|
+
return { ok: true, assets: res.data.map((a) => ({ id: a.id, tags: a.tags })) };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Helper for tests / future swap-in: enumerator backed by an in-memory list. */
|
|
39
|
+
export function fixedAssetEnumerator(assets) {
|
|
40
|
+
return {
|
|
41
|
+
enumerateAssets: async () => ({ ok: true, assets: [...assets] }),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ── Search adapter (selection 'query' path) ──────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Default unavailable adapter — kept for tests and for the
|
|
47
|
+
* pre-Typesense-provisioned mode.
|
|
48
|
+
*/
|
|
49
|
+
export function buildSearchAdapter(_client) {
|
|
50
|
+
return {
|
|
51
|
+
searchAssets: async () => ({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: createToolError('UPSTREAM', 'Asset search is not available: the public Typesense host has not ' +
|
|
54
|
+
'been provisioned yet. Use `ids` or `collection_id` selection.'),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Search adapter backed by the Collage Nuxt search proxy. Bridges the
|
|
60
|
+
* bulk-tag tools' `AssetSearchAdapter` interface to the workspace-scoped
|
|
61
|
+
* `search_assets` runner. Returns up to the first page (per_page=250) of
|
|
62
|
+
* hits, projected down to the `ResolvedAsset` shape expected by the
|
|
63
|
+
* resolver. The `tags` array is only present when the index actually
|
|
64
|
+
* has tags indexed for the row — empty when the indexer has not yet
|
|
65
|
+
* caught up.
|
|
66
|
+
*/
|
|
67
|
+
export function buildTypesenseSearchAdapter(searchClient, workspaceId) {
|
|
68
|
+
return {
|
|
69
|
+
searchAssets: async (query) => {
|
|
70
|
+
const requestId = generateRequestId();
|
|
71
|
+
const result = await runSearchAssets(searchClient, workspaceId, {
|
|
72
|
+
q: query,
|
|
73
|
+
page: 1,
|
|
74
|
+
per_page: 250,
|
|
75
|
+
sort_by: '_text_match:desc,modified_at:desc',
|
|
76
|
+
projection: 'verbose',
|
|
77
|
+
}, requestId);
|
|
78
|
+
if (result.isError === true) {
|
|
79
|
+
const sc = result.structuredContent;
|
|
80
|
+
const err = sc.error ?? { code: 'UPSTREAM', message: 'unknown search failure', retriable: true };
|
|
81
|
+
return { ok: false, error: createToolError(err.code, err.message) };
|
|
82
|
+
}
|
|
83
|
+
const sc = result.structuredContent;
|
|
84
|
+
const assets = [];
|
|
85
|
+
for (const hit of sc.hits) {
|
|
86
|
+
const asset = { id: String(hit.id) };
|
|
87
|
+
if (hit.display_file_name !== null && hit.display_file_name !== undefined) {
|
|
88
|
+
asset.display_file_name = hit.display_file_name;
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(hit.tags)) {
|
|
91
|
+
asset.tags = hit.tags;
|
|
92
|
+
}
|
|
93
|
+
assets.push(asset);
|
|
94
|
+
}
|
|
95
|
+
return { ok: true, assets };
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
// ── rename_asset client ──────────────────────────────────────────────
|
|
100
|
+
export function buildRenameAssetClient(client) {
|
|
101
|
+
return {
|
|
102
|
+
getCurrentName: async (assetId) => {
|
|
103
|
+
const res = await client.getAssetDetails(assetId);
|
|
104
|
+
if (!res.ok)
|
|
105
|
+
return null;
|
|
106
|
+
const name = res.data.display_file_name;
|
|
107
|
+
return name === '' ? null : name;
|
|
108
|
+
},
|
|
109
|
+
renameAsset: async (assetId, newName) => {
|
|
110
|
+
const res = await client.renameAsset(assetId, newName);
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
return { ok: false, error: res.error };
|
|
113
|
+
return { ok: true };
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
// ── update_asset_metadata client ─────────────────────────────────────
|
|
118
|
+
/**
|
|
119
|
+
* `view-detail` carries `display_file_name` and `description` directly. We
|
|
120
|
+
* map `display_file_name` onto the tool's `title` slot (the
|
|
121
|
+
* Admin-Frontend rename UI does the same — there is no separate "title"
|
|
122
|
+
* column in `digital_assets`).
|
|
123
|
+
*
|
|
124
|
+
* Custom fields come from a separate `get-custom-fields` call, projected
|
|
125
|
+
* by `name` (with `field_name` as fallback) into `Record<string, CustomFieldValue>`.
|
|
126
|
+
* Update side: we re-fetch the same projection, overwrite each `value` for
|
|
127
|
+
* the requested keys, and POST the full array back via `update-custom-fields`.
|
|
128
|
+
*/
|
|
129
|
+
export function buildUpdateAssetMetadataClient(client) {
|
|
130
|
+
return {
|
|
131
|
+
getMetadata: async (assetId) => {
|
|
132
|
+
const detail = await client.getAssetDetails(assetId);
|
|
133
|
+
if (!detail.ok)
|
|
134
|
+
return { ok: false, error: detail.error };
|
|
135
|
+
const fields = await client.getAssetCustomFields(assetId);
|
|
136
|
+
const customFields = fields.ok
|
|
137
|
+
? projectCustomFieldsToValues(fields.data)
|
|
138
|
+
: {};
|
|
139
|
+
const snapshot = {
|
|
140
|
+
title: snapshotTitle(detail.data),
|
|
141
|
+
description: detail.data.description ?? null,
|
|
142
|
+
custom_fields: customFields,
|
|
143
|
+
};
|
|
144
|
+
return { ok: true, snapshot };
|
|
145
|
+
},
|
|
146
|
+
updateMetadata: async (assetId, patch) => {
|
|
147
|
+
const errors = [];
|
|
148
|
+
if (patch.title !== undefined) {
|
|
149
|
+
const r = await client.updateAssetField(assetId, 'display_file_name', patch.title);
|
|
150
|
+
if (!r.ok)
|
|
151
|
+
errors.push(r.error);
|
|
152
|
+
}
|
|
153
|
+
if (patch.description !== undefined) {
|
|
154
|
+
const r = await client.updateAssetField(assetId, 'description', patch.description);
|
|
155
|
+
if (!r.ok)
|
|
156
|
+
errors.push(r.error);
|
|
157
|
+
}
|
|
158
|
+
if (patch.custom_fields !== undefined) {
|
|
159
|
+
// Re-fetch the projection so we can overwrite values by name and
|
|
160
|
+
// round-trip the full row shape the upstream expects.
|
|
161
|
+
const current = await client.getAssetCustomFields(assetId);
|
|
162
|
+
if (!current.ok) {
|
|
163
|
+
errors.push(current.error);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const updated = mergeCustomFieldUpdates(current.data, patch.custom_fields);
|
|
167
|
+
const r = await client.updateAssetCustomFields(assetId, updated);
|
|
168
|
+
if (!r.ok)
|
|
169
|
+
errors.push(r.error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (errors.length > 0) {
|
|
173
|
+
const first = errors[0];
|
|
174
|
+
if (first === undefined) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
error: createToolError('INTERNAL', 'metadata update failed without a specific error'),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { ok: false, error: first };
|
|
181
|
+
}
|
|
182
|
+
return { ok: true };
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function snapshotTitle(detail) {
|
|
187
|
+
const name = detail.display_file_name;
|
|
188
|
+
return name === '' ? null : name;
|
|
189
|
+
}
|
|
190
|
+
function projectCustomFieldsToValues(fields) {
|
|
191
|
+
const out = {};
|
|
192
|
+
for (const f of fields) {
|
|
193
|
+
const key = f.name ?? f.field_name;
|
|
194
|
+
if (key === undefined)
|
|
195
|
+
continue;
|
|
196
|
+
out[key] = coerceToCustomFieldValue(f.value);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
function coerceToCustomFieldValue(value) {
|
|
201
|
+
if (value === null || value === undefined)
|
|
202
|
+
return null;
|
|
203
|
+
if (typeof value === 'string' ||
|
|
204
|
+
typeof value === 'number' ||
|
|
205
|
+
typeof value === 'boolean') {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(value)) {
|
|
209
|
+
const arr = [];
|
|
210
|
+
for (const v of value) {
|
|
211
|
+
if (typeof v === 'string' || typeof v === 'number')
|
|
212
|
+
arr.push(v);
|
|
213
|
+
}
|
|
214
|
+
return arr;
|
|
215
|
+
}
|
|
216
|
+
// Fall back to JSON string so the diff renderer has *something* to show.
|
|
217
|
+
return JSON.stringify(value);
|
|
218
|
+
}
|
|
219
|
+
function mergeCustomFieldUpdates(current, patch) {
|
|
220
|
+
return current.map((f) => {
|
|
221
|
+
const key = f.name ?? f.field_name;
|
|
222
|
+
const next = key !== undefined && key in patch ? patch[key] : undefined;
|
|
223
|
+
if (next === undefined) {
|
|
224
|
+
// Untouched — pass through the row as the upstream expects.
|
|
225
|
+
return { ...f };
|
|
226
|
+
}
|
|
227
|
+
return { ...f, value: next };
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// ── bulk_*_tags client ───────────────────────────────────────────────
|
|
231
|
+
/**
|
|
232
|
+
* Tag read uses `view-detail.tags` (array of `{id, tag_name}`). Write side:
|
|
233
|
+
* `setTags` is a full-replace — diff against current, add new via
|
|
234
|
+
* `add-tags-to-multiple-file`, remove others via
|
|
235
|
+
* `delete-tag-from-multiple-file` (called once per removed tag because the
|
|
236
|
+
* upstream takes a single `tag_name` per request).
|
|
237
|
+
*/
|
|
238
|
+
export function buildBulkTagsClient(client) {
|
|
239
|
+
return {
|
|
240
|
+
getCurrentTags: async (assetId) => {
|
|
241
|
+
const res = await client.getAssetDetails(assetId);
|
|
242
|
+
if (!res.ok)
|
|
243
|
+
return { ok: false, error: res.error };
|
|
244
|
+
const names = (res.data.tags ?? []).map((t) => t.tag_name);
|
|
245
|
+
return { ok: true, tags: names };
|
|
246
|
+
},
|
|
247
|
+
setTags: async (assetId, tags) => {
|
|
248
|
+
const current = await client.getAssetDetails(assetId);
|
|
249
|
+
if (!current.ok)
|
|
250
|
+
return { ok: false, error: current.error };
|
|
251
|
+
const currentNames = (current.data.tags ?? []).map((t) => t.tag_name);
|
|
252
|
+
const wanted = new Set(tags);
|
|
253
|
+
const have = new Set(currentNames);
|
|
254
|
+
const toAdd = tags.filter((t) => !have.has(t));
|
|
255
|
+
const toRemove = currentNames.filter((t) => !wanted.has(t));
|
|
256
|
+
if (toAdd.length > 0) {
|
|
257
|
+
const r = await client.addTagsToAssets([assetId], toAdd);
|
|
258
|
+
if (!r.ok)
|
|
259
|
+
return { ok: false, error: r.error };
|
|
260
|
+
}
|
|
261
|
+
for (const name of toRemove) {
|
|
262
|
+
const r = await client.removeTagFromAssets([assetId], name);
|
|
263
|
+
if (!r.ok)
|
|
264
|
+
return { ok: false, error: r.error };
|
|
265
|
+
}
|
|
266
|
+
return { ok: true };
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
// ── create_collection client ─────────────────────────────────────────
|
|
271
|
+
export function buildCreateCollectionClient(client) {
|
|
272
|
+
return {
|
|
273
|
+
createCollection: async (input) => {
|
|
274
|
+
const res = await client.createCollection({
|
|
275
|
+
name: input.name,
|
|
276
|
+
description: input.description,
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok)
|
|
279
|
+
return { ok: false, error: res.error };
|
|
280
|
+
return { ok: true, collection_id: String(res.data.id) };
|
|
281
|
+
},
|
|
282
|
+
addAssetsToCollection: async (collectionId, assetIds) => {
|
|
283
|
+
const res = await client.addAssetsToCollection(collectionId, assetIds);
|
|
284
|
+
if (!res.ok)
|
|
285
|
+
return { ok: false, error: res.error };
|
|
286
|
+
return { ok: true };
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// ── update_collection client ─────────────────────────────────────────
|
|
291
|
+
export function buildUpdateCollectionClient(client) {
|
|
292
|
+
return {
|
|
293
|
+
getCollectionMetadata: async (collectionId) => {
|
|
294
|
+
// CollageClient exposes listCollections; locate the target by id.
|
|
295
|
+
const res = await client.listCollections();
|
|
296
|
+
if (!res.ok)
|
|
297
|
+
return { ok: false, error: res.error };
|
|
298
|
+
const match = res.data.find((c) => String(c.id) === collectionId);
|
|
299
|
+
if (match === undefined) {
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
error: createToolError('NOT_FOUND', `Collection ${collectionId} not found in workspace.`),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
ok: true,
|
|
307
|
+
snapshot: {
|
|
308
|
+
name: match.name ?? null,
|
|
309
|
+
description: match.description ?? null,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
updateCollectionMetadata: async (collectionId, patch) => {
|
|
314
|
+
const res = await client.updateCollection(collectionId, patch);
|
|
315
|
+
if (!res.ok)
|
|
316
|
+
return { ok: false, error: res.error };
|
|
317
|
+
return { ok: true };
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// ── add/remove_to_collection client ──────────────────────────────────
|
|
322
|
+
export function buildCollectionMembershipClient(client) {
|
|
323
|
+
return {
|
|
324
|
+
getCollectionAssetIds: async (collectionId) => {
|
|
325
|
+
const res = await client.listCollections();
|
|
326
|
+
if (!res.ok)
|
|
327
|
+
return { ok: false, error: res.error };
|
|
328
|
+
const match = res.data.find((c) => String(c.id) === collectionId);
|
|
329
|
+
if (match === undefined) {
|
|
330
|
+
return {
|
|
331
|
+
ok: false,
|
|
332
|
+
error: createToolError('NOT_FOUND', `Collection ${collectionId} not found in workspace.`),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const ids = (match.assets_id ?? []).map((id) => String(id));
|
|
336
|
+
return { ok: true, asset_ids: ids };
|
|
337
|
+
},
|
|
338
|
+
addAssetsToCollection: async (collectionId, assetIds) => {
|
|
339
|
+
const res = await client.addAssetsToCollection(collectionId, assetIds);
|
|
340
|
+
if (!res.ok)
|
|
341
|
+
return { ok: false, error: res.error };
|
|
342
|
+
return { ok: true };
|
|
343
|
+
},
|
|
344
|
+
removeAssetsFromCollection: async (collectionId, assetIds) => {
|
|
345
|
+
const res = await client.removeAssetsFromCollection(collectionId, assetIds);
|
|
346
|
+
if (!res.ok)
|
|
347
|
+
return { ok: false, error: res.error };
|
|
348
|
+
return { ok: true };
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
export function buildCreateShareLinkClient(client) {
|
|
353
|
+
return {
|
|
354
|
+
createShareLink: async (request) => {
|
|
355
|
+
// Map the dry-run-resolved request to the right upstream endpoint:
|
|
356
|
+
// collection target → POST collection/generate-share-url,
|
|
357
|
+
// asset target → POST dashboard/generate-share-assets-url.
|
|
358
|
+
const sharedFields = {
|
|
359
|
+
title: request.title,
|
|
360
|
+
description: request.description,
|
|
361
|
+
password: request.password,
|
|
362
|
+
hide_download: request.hide_download,
|
|
363
|
+
expiration: request.expiration,
|
|
364
|
+
};
|
|
365
|
+
if (request.target_kind === 'collection' && request.collection_id !== undefined) {
|
|
366
|
+
const res = await client.generateCollectionShareUrl({
|
|
367
|
+
collection_id: request.collection_id,
|
|
368
|
+
assets: request.asset_ids,
|
|
369
|
+
...sharedFields,
|
|
370
|
+
});
|
|
371
|
+
if (!res.ok)
|
|
372
|
+
return { ok: false, error: res.error };
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
share_id: String(res.data.id),
|
|
376
|
+
share_url: res.data.share_url,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
const res = await client.generateShareAssetsUrl({
|
|
380
|
+
assets: request.asset_ids,
|
|
381
|
+
category: [],
|
|
382
|
+
...sharedFields,
|
|
383
|
+
});
|
|
384
|
+
if (!res.ok)
|
|
385
|
+
return { ok: false, error: res.error };
|
|
386
|
+
return {
|
|
387
|
+
ok: true,
|
|
388
|
+
share_id: String(res.data.id),
|
|
389
|
+
share_url: res.data.share_url,
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// ── revoke_share_link client ─────────────────────────────────────────
|
|
395
|
+
export function buildRevokeShareLinkClient(client) {
|
|
396
|
+
return {
|
|
397
|
+
getShareLink: async (shareId) => {
|
|
398
|
+
const res = await client.getShareLink(shareId);
|
|
399
|
+
if (!res.ok)
|
|
400
|
+
return { ok: false, error: res.error };
|
|
401
|
+
const row = res.data;
|
|
402
|
+
const password = row.password;
|
|
403
|
+
const passwordProtected = typeof password === 'string' && password.length > 0;
|
|
404
|
+
const snapshot = {
|
|
405
|
+
id: String(row.id),
|
|
406
|
+
title: row.title ?? null,
|
|
407
|
+
share_url: row.share_url ?? row.share_new_url ?? null,
|
|
408
|
+
expiration: row.expiration ?? null,
|
|
409
|
+
password_protected: passwordProtected,
|
|
410
|
+
};
|
|
411
|
+
return { ok: true, snapshot };
|
|
412
|
+
},
|
|
413
|
+
revokeShareLink: async (shareId) => {
|
|
414
|
+
const res = await client.revokeShareLink(shareId);
|
|
415
|
+
if (!res.ok)
|
|
416
|
+
return { ok: false, error: res.error };
|
|
417
|
+
return { ok: true };
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
// ── create_folder / rename_folder / move_asset clients ───────────────
|
|
422
|
+
export function buildCreateFolderClient(client) {
|
|
423
|
+
return {
|
|
424
|
+
createFolder: async (input) => {
|
|
425
|
+
const res = await client.createCategory({
|
|
426
|
+
folder_name: input.folder_name,
|
|
427
|
+
parent_id: input.parent_id,
|
|
428
|
+
});
|
|
429
|
+
if (!res.ok)
|
|
430
|
+
return { ok: false, error: res.error };
|
|
431
|
+
return { ok: true, folder_id: String(res.data.id) };
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
export function buildRenameFolderClient(client) {
|
|
436
|
+
return {
|
|
437
|
+
getCurrentFolderName: async (categoryId) => {
|
|
438
|
+
const list = await client.listAllCategoriesRecursive();
|
|
439
|
+
if (!list.ok)
|
|
440
|
+
return null;
|
|
441
|
+
const found = list.data.find((c) => String(c.id) === categoryId);
|
|
442
|
+
return found?.folder_name ?? null;
|
|
443
|
+
},
|
|
444
|
+
renameFolder: async (input) => {
|
|
445
|
+
const renameInput = {
|
|
446
|
+
folder_name: input.folder_name,
|
|
447
|
+
};
|
|
448
|
+
if (input.description !== undefined) {
|
|
449
|
+
renameInput.description = input.description;
|
|
450
|
+
}
|
|
451
|
+
const res = await client.renameCategory(input.category_id, renameInput);
|
|
452
|
+
if (!res.ok)
|
|
453
|
+
return { ok: false, error: res.error };
|
|
454
|
+
return { ok: true };
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
export function buildMoveAssetClient(client) {
|
|
459
|
+
return {
|
|
460
|
+
moveAsset: async (input) => {
|
|
461
|
+
const res = await client.moveAssetsAndFolders({
|
|
462
|
+
asset_ids: [input.asset_id],
|
|
463
|
+
folder_ids: [],
|
|
464
|
+
destination_id: input.destination_category_id,
|
|
465
|
+
});
|
|
466
|
+
if (!res.ok)
|
|
467
|
+
return { ok: false, error: res.error };
|
|
468
|
+
return { ok: true };
|
|
469
|
+
},
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
// ── analyze_share_links enumerator ───────────────────────────────────
|
|
473
|
+
//
|
|
474
|
+
// Walks `client.listShareLinks` once per status bucket
|
|
475
|
+
// (active / expired / revoked) so each row can be tagged with its
|
|
476
|
+
// upstream-derived status. Pages until the upstream paginator is
|
|
477
|
+
// exhausted or the internal cap (`ENUMERATOR_HARD_CAP`) is hit. When
|
|
478
|
+
// the cap is reached, the partial dataset is returned with
|
|
479
|
+
// `truncated: true`; the analyzer surfaces that to the caller as
|
|
480
|
+
// `report.truncated` so downstream renderers can flag the partial
|
|
481
|
+
// result.
|
|
482
|
+
//
|
|
483
|
+
// Why per-status fetches: the live `ShareLink` schema does not expose a
|
|
484
|
+
// `status` column; the only way to discriminate active vs expired vs
|
|
485
|
+
// revoked is to query each upstream `filter_by` value separately. This
|
|
486
|
+
// fans out to three list calls per analysis — acceptable for the read-
|
|
487
|
+
// only use case and easily memoized by the T5 cache layer.
|
|
488
|
+
const SHARE_LINK_PAGE_SIZE = 200;
|
|
489
|
+
export function buildShareLinkAnalyticsEnumerator(client) {
|
|
490
|
+
return {
|
|
491
|
+
enumerate: async () => {
|
|
492
|
+
const statuses = ['active', 'expired', 'revoked'];
|
|
493
|
+
const collected = [];
|
|
494
|
+
let truncated = false;
|
|
495
|
+
let lastError = null;
|
|
496
|
+
let anyOk = false;
|
|
497
|
+
for (const status of statuses) {
|
|
498
|
+
if (collected.length >= ENUMERATOR_HARD_CAP) {
|
|
499
|
+
truncated = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
let page = 1;
|
|
503
|
+
// Per-status pagination loop.
|
|
504
|
+
// eslint-disable-next-line no-constant-condition
|
|
505
|
+
while (true) {
|
|
506
|
+
const res = await client.listShareLinks({
|
|
507
|
+
page,
|
|
508
|
+
pageSize: SHARE_LINK_PAGE_SIZE,
|
|
509
|
+
filterBy: status,
|
|
510
|
+
});
|
|
511
|
+
if (!res.ok) {
|
|
512
|
+
// Track per-status failures so a single 4xx (e.g. 'revoked'
|
|
513
|
+
// unsupported) does not mask successful status fetches. If
|
|
514
|
+
// every status fails, surface the last error.
|
|
515
|
+
lastError = res.error;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
anyOk = true;
|
|
519
|
+
const rows = res.data.data;
|
|
520
|
+
for (const row of rows) {
|
|
521
|
+
if (collected.length >= ENUMERATOR_HARD_CAP) {
|
|
522
|
+
truncated = true;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
collected.push(projectShareLinkToRow(row, status));
|
|
526
|
+
}
|
|
527
|
+
if (truncated)
|
|
528
|
+
break;
|
|
529
|
+
// Stop when we've reached the last page or the upstream
|
|
530
|
+
// returned fewer rows than the requested page size.
|
|
531
|
+
const lastPage = res.data.last_page;
|
|
532
|
+
if (lastPage !== undefined && page >= lastPage)
|
|
533
|
+
break;
|
|
534
|
+
if (rows.length < SHARE_LINK_PAGE_SIZE)
|
|
535
|
+
break;
|
|
536
|
+
page += 1;
|
|
537
|
+
}
|
|
538
|
+
if (truncated)
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
if (!anyOk && lastError !== null) {
|
|
542
|
+
return { ok: false, error: lastError };
|
|
543
|
+
}
|
|
544
|
+
return { ok: true, rows: collected, truncated };
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function projectShareLinkToRow(row, status) {
|
|
549
|
+
// `total_visited` may be number | string | null per upstream schema.
|
|
550
|
+
const viewCountRaw = row.total_visited;
|
|
551
|
+
let viewCount = 0;
|
|
552
|
+
if (typeof viewCountRaw === 'number' && Number.isFinite(viewCountRaw)) {
|
|
553
|
+
viewCount = viewCountRaw;
|
|
554
|
+
}
|
|
555
|
+
else if (typeof viewCountRaw === 'string') {
|
|
556
|
+
const parsed = Number(viewCountRaw);
|
|
557
|
+
if (Number.isFinite(parsed))
|
|
558
|
+
viewCount = parsed;
|
|
559
|
+
}
|
|
560
|
+
const assetIds = (row.asset_ids ?? []).map((id) => String(id));
|
|
561
|
+
// `ShareLink` does not yet model `collection_id` — it's a passthrough
|
|
562
|
+
// field. Read it defensively without a runtime cast to `any`.
|
|
563
|
+
const collectionIdRaw = row['collection_id'];
|
|
564
|
+
const collectionId = typeof collectionIdRaw === 'string' || typeof collectionIdRaw === 'number'
|
|
565
|
+
? String(collectionIdRaw)
|
|
566
|
+
: null;
|
|
567
|
+
// `expiration` is the upstream's expires_at field (string | null).
|
|
568
|
+
const expiresAt = typeof row.expiration === 'string' && row.expiration.length > 0
|
|
569
|
+
? row.expiration
|
|
570
|
+
: null;
|
|
571
|
+
const createdAt = typeof row.created_at === 'string' && row.created_at.length > 0
|
|
572
|
+
? row.created_at
|
|
573
|
+
: null;
|
|
574
|
+
const userId = row.user_id;
|
|
575
|
+
const createdByUserId = typeof userId === 'string' || typeof userId === 'number'
|
|
576
|
+
? String(userId)
|
|
577
|
+
: null;
|
|
578
|
+
return {
|
|
579
|
+
id: String(row.id),
|
|
580
|
+
title: typeof row.title === 'string' ? row.title : null,
|
|
581
|
+
share_url: typeof row.share_url === 'string' && row.share_url.length > 0
|
|
582
|
+
? row.share_url
|
|
583
|
+
: typeof row.share_new_url === 'string' && row.share_new_url.length > 0
|
|
584
|
+
? row.share_new_url
|
|
585
|
+
: null,
|
|
586
|
+
view_count: viewCount,
|
|
587
|
+
created_at: createdAt,
|
|
588
|
+
expires_at: expiresAt,
|
|
589
|
+
status,
|
|
590
|
+
created_by_user_id: createdByUserId,
|
|
591
|
+
asset_ids: assetIds,
|
|
592
|
+
collection_id: collectionId,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// ── report_asset_activation dashboard reader ─────────────────────────
|
|
596
|
+
//
|
|
597
|
+
// `getDashboardCommonData()` returns `unknown` at the client layer. The
|
|
598
|
+
// reader narrows it through a small zod schema covering only the fields
|
|
599
|
+
// the activation report consumes. When the parse fails (i.e. the
|
|
600
|
+
// upstream shape changes or the workspace is in a degraded state that
|
|
601
|
+
// returns a different envelope), the reader returns `ok: true,
|
|
602
|
+
// snapshot: null` so the report can degrade gracefully to
|
|
603
|
+
// `total_assets: null` rather than failing.
|
|
604
|
+
import { z } from 'zod';
|
|
605
|
+
/**
|
|
606
|
+
* Narrow projection of `dashboard/common-data`. The upstream envelope
|
|
607
|
+
* carries dozens of opaque keys; we only require `total_assets` to be a
|
|
608
|
+
* non-negative integer. Several plausible field names are accepted to
|
|
609
|
+
* tolerate small upstream renames (the live Admin-Frontend reads either
|
|
610
|
+
* `total_assets` or `total_record` depending on the dashboard widget).
|
|
611
|
+
*/
|
|
612
|
+
const DashboardCommonDataSchema = z
|
|
613
|
+
.object({
|
|
614
|
+
total_assets: z.number().int().nonnegative().optional(),
|
|
615
|
+
total_record: z.number().int().nonnegative().optional(),
|
|
616
|
+
data: z
|
|
617
|
+
.object({
|
|
618
|
+
total_assets: z.number().int().nonnegative().optional(),
|
|
619
|
+
total_record: z.number().int().nonnegative().optional(),
|
|
620
|
+
})
|
|
621
|
+
.passthrough()
|
|
622
|
+
.optional(),
|
|
623
|
+
})
|
|
624
|
+
.passthrough();
|
|
625
|
+
export function buildDashboardCommonDataReader(client) {
|
|
626
|
+
return {
|
|
627
|
+
read: async () => {
|
|
628
|
+
const res = await client.getDashboardCommonData();
|
|
629
|
+
if (!res.ok)
|
|
630
|
+
return { ok: false, error: res.error };
|
|
631
|
+
const parsed = DashboardCommonDataSchema.safeParse(res.data);
|
|
632
|
+
if (!parsed.success) {
|
|
633
|
+
// Shape regressed — surface as null so the report can degrade.
|
|
634
|
+
return { ok: true, snapshot: null };
|
|
635
|
+
}
|
|
636
|
+
const total = parsed.data.total_assets ??
|
|
637
|
+
parsed.data.total_record ??
|
|
638
|
+
parsed.data.data?.total_assets ??
|
|
639
|
+
parsed.data.data?.total_record ??
|
|
640
|
+
null;
|
|
641
|
+
if (total === null)
|
|
642
|
+
return { ok: true, snapshot: null };
|
|
643
|
+
const snapshot = { total_assets: total };
|
|
644
|
+
return { ok: true, snapshot };
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
//# sourceMappingURL=client-adapters.js.map
|