@happyvertical/smrt-assets 0.30.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/AGENTS.md +78 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +136 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/asset-association.d.ts +16 -0
- package/dist/asset-association.d.ts.map +1 -0
- package/dist/asset-associations.d.ts +27 -0
- package/dist/asset-associations.d.ts.map +1 -0
- package/dist/asset-capabilities.d.ts +137 -0
- package/dist/asset-capabilities.d.ts.map +1 -0
- package/dist/asset-conventions.d.ts +76 -0
- package/dist/asset-conventions.d.ts.map +1 -0
- package/dist/asset-metafield.d.ts +27 -0
- package/dist/asset-metafield.d.ts.map +1 -0
- package/dist/asset-metafields.d.ts +27 -0
- package/dist/asset-metafields.d.ts.map +1 -0
- package/dist/asset-runtime.d.ts +218 -0
- package/dist/asset-runtime.d.ts.map +1 -0
- package/dist/asset-serving.d.ts +146 -0
- package/dist/asset-serving.d.ts.map +1 -0
- package/dist/asset-status.d.ts +15 -0
- package/dist/asset-status.d.ts.map +1 -0
- package/dist/asset-statuses.d.ts +25 -0
- package/dist/asset-statuses.d.ts.map +1 -0
- package/dist/asset-store.d.ts +200 -0
- package/dist/asset-store.d.ts.map +1 -0
- package/dist/asset-type.d.ts +15 -0
- package/dist/asset-type.d.ts.map +1 -0
- package/dist/asset-types.d.ts +28 -0
- package/dist/asset-types.d.ts.map +1 -0
- package/dist/asset.d.ts +158 -0
- package/dist/asset.d.ts.map +1 -0
- package/dist/assets.d.ts +125 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/folder.d.ts +16 -0
- package/dist/folder.d.ts.map +1 -0
- package/dist/folders.d.ts +45 -0
- package/dist/folders.d.ts.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2285 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.json +4079 -0
- package/dist/media-bundle-persistence.d.ts +99 -0
- package/dist/media-bundle-persistence.d.ts.map +1 -0
- package/dist/owned-asset-helpers.d.ts +20 -0
- package/dist/owned-asset-helpers.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +127 -0
- package/dist/playground.js.map +1 -0
- package/dist/smrt-knowledge.json +1922 -0
- package/dist/svelte/ActionBar.svelte +203 -0
- package/dist/svelte/ActionBar.svelte.d.ts +5 -0
- package/dist/svelte/ActionBar.svelte.d.ts.map +1 -0
- package/dist/svelte/AssetDetail.svelte +521 -0
- package/dist/svelte/AssetDetail.svelte.d.ts +35 -0
- package/dist/svelte/AssetDetail.svelte.d.ts.map +1 -0
- package/dist/svelte/AssetGrid.svelte +351 -0
- package/dist/svelte/AssetGrid.svelte.d.ts +5 -0
- package/dist/svelte/AssetGrid.svelte.d.ts.map +1 -0
- package/dist/svelte/AssetList.svelte +436 -0
- package/dist/svelte/AssetList.svelte.d.ts +5 -0
- package/dist/svelte/AssetList.svelte.d.ts.map +1 -0
- package/dist/svelte/AssetManager.svelte +381 -0
- package/dist/svelte/AssetManager.svelte.d.ts +5 -0
- package/dist/svelte/AssetManager.svelte.d.ts.map +1 -0
- package/dist/svelte/AssetToolbar.svelte +388 -0
- package/dist/svelte/AssetToolbar.svelte.d.ts +5 -0
- package/dist/svelte/AssetToolbar.svelte.d.ts.map +1 -0
- package/dist/svelte/CreateAssetModal.svelte +373 -0
- package/dist/svelte/CreateAssetModal.svelte.d.ts +19 -0
- package/dist/svelte/CreateAssetModal.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/ActionBar.test.js +72 -0
- package/dist/svelte/__tests__/AssetDetail.test.js +57 -0
- package/dist/svelte/__tests__/AssetGrid.test.js +69 -0
- package/dist/svelte/__tests__/AssetList.test.js +72 -0
- package/dist/svelte/__tests__/AssetManager.test.js +21 -0
- package/dist/svelte/__tests__/AssetManagerRoute.test.js +16 -0
- package/dist/svelte/__tests__/AssetToolbar.test.js +39 -0
- package/dist/svelte/__tests__/CreateAssetModal.test.js +42 -0
- package/dist/svelte/i18n.d.ts +76 -0
- package/dist/svelte/i18n.d.ts.map +1 -0
- package/dist/svelte/i18n.js +87 -0
- package/dist/svelte/index.d.ts +19 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +30 -0
- package/dist/svelte/playground/AssetDetailPreview.svelte +131 -0
- package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts +8 -0
- package/dist/svelte/playground/AssetDetailPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/CreateAssetModalPreview.svelte +151 -0
- package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CreateAssetModalPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +60 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +93 -0
- package/dist/svelte/routes/AssetManagerRoute.svelte +209 -0
- package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts +4 -0
- package/dist/svelte/routes/AssetManagerRoute.svelte.d.ts.map +1 -0
- package/dist/svelte/routes/index.d.ts +2 -0
- package/dist/svelte/routes/index.d.ts.map +1 -0
- package/dist/svelte/routes/index.js +1 -0
- package/dist/svelte/routes/shared.d.ts +25 -0
- package/dist/svelte/routes/shared.d.ts.map +1 -0
- package/dist/svelte/routes/shared.js +31 -0
- package/dist/svelte/types.d.ts +179 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +6 -0
- package/dist/types.d.ts +80 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +10 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +85 -0
- package/dist/ui.js.map +1 -0
- package/package.json +102 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2285 @@
|
|
|
1
|
+
import { ObjectRegistry, foreignKey, smrt, SmrtPolymorphicAssociation, SmrtJunction, SmrtObject, crossPackageRef, SmrtCollection, SmrtHierarchical } from "@happyvertical/smrt-core";
|
|
2
|
+
import { Tag } from "@happyvertical/smrt-tags";
|
|
3
|
+
import { tenantId, TenantScoped, withSystemContext } from "@happyvertical/smrt-tenancy";
|
|
4
|
+
import { getFilesystem, FileNotFoundError } from "@happyvertical/files";
|
|
5
|
+
ObjectRegistry.registerPackageManifest(
|
|
6
|
+
new URL("./manifest.json", import.meta.url)
|
|
7
|
+
);
|
|
8
|
+
var __defProp$3 = Object.defineProperty;
|
|
9
|
+
var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
|
|
10
|
+
var __decorateClass$6 = (decorators, target, key, kind) => {
|
|
11
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
|
|
12
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
13
|
+
if (decorator = decorators[i])
|
|
14
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
15
|
+
if (kind && result) __defProp$3(target, key, result);
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
let AssetAssociation = class extends SmrtPolymorphicAssociation {
|
|
19
|
+
tenantId = null;
|
|
20
|
+
assetId = "";
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super(options);
|
|
23
|
+
if (options.assetId) this.assetId = options.assetId;
|
|
24
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
__decorateClass$6([
|
|
28
|
+
tenantId({ nullable: true })
|
|
29
|
+
], AssetAssociation.prototype, "tenantId", 2);
|
|
30
|
+
__decorateClass$6([
|
|
31
|
+
foreignKey("Asset", { required: true })
|
|
32
|
+
], AssetAssociation.prototype, "assetId", 2);
|
|
33
|
+
AssetAssociation = __decorateClass$6([
|
|
34
|
+
TenantScoped({ mode: "optional" }),
|
|
35
|
+
smrt({
|
|
36
|
+
conflictColumns: ["asset_id", "meta_type", "meta_id", "role"],
|
|
37
|
+
api: { include: ["list", "get", "create", "delete"] },
|
|
38
|
+
mcp: { include: ["list", "get", "create"] },
|
|
39
|
+
cli: true
|
|
40
|
+
})
|
|
41
|
+
], AssetAssociation);
|
|
42
|
+
var __defProp$2 = Object.defineProperty;
|
|
43
|
+
var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
|
|
44
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
45
|
+
var __decorateClass$5 = (decorators, target, key, kind) => {
|
|
46
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
|
|
47
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
48
|
+
if (decorator = decorators[i])
|
|
49
|
+
result = decorator(result) || result;
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, key + "", value);
|
|
53
|
+
let AssetAssociationCollection = class extends SmrtJunction {
|
|
54
|
+
// Composite left = (metaType, metaId); right = assetId.
|
|
55
|
+
// `leftField` placeholder satisfies the abstract base — the overrides below
|
|
56
|
+
// always handle the composite key explicitly.
|
|
57
|
+
leftField = "metaId";
|
|
58
|
+
rightField = "assetId";
|
|
59
|
+
/**
|
|
60
|
+
* List associations for a polymorphic owner.
|
|
61
|
+
*
|
|
62
|
+
* Composite left key — pass both halves.
|
|
63
|
+
*/
|
|
64
|
+
// @ts-expect-error — diverges from SmrtJunction.byLeft(leftId) by arity; see class docstring.
|
|
65
|
+
async byLeft(metaType, metaId, opts = {}) {
|
|
66
|
+
return await this.list({
|
|
67
|
+
// Spread opts first so the fixed polymorphic owner keys always win.
|
|
68
|
+
where: { ...opts, metaType, metaId },
|
|
69
|
+
orderBy: "sort_order ASC"
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Create an association. Composite left (metaType, metaId) precedes right (assetId).
|
|
74
|
+
*/
|
|
75
|
+
// @ts-expect-error — diverges from SmrtJunction.attach(leftId, rightId) by arity.
|
|
76
|
+
async attach(metaType, metaId, assetId, opts = {}) {
|
|
77
|
+
return await this.create({
|
|
78
|
+
// Spread opts first so the fixed key fields always win.
|
|
79
|
+
...opts,
|
|
80
|
+
assetId,
|
|
81
|
+
metaType,
|
|
82
|
+
metaId
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete matching associations. Composite left (metaType, metaId) precedes right (assetId).
|
|
87
|
+
*/
|
|
88
|
+
// @ts-expect-error — diverges from SmrtJunction.detach(leftId, rightId) by arity.
|
|
89
|
+
async detach(metaType, metaId, assetId, opts = {}) {
|
|
90
|
+
const links = await this.list({
|
|
91
|
+
where: { ...opts, metaType, metaId, assetId }
|
|
92
|
+
});
|
|
93
|
+
for (const link of links) {
|
|
94
|
+
await link.delete();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Replace all associations for a polymorphic owner with the given asset IDs.
|
|
99
|
+
* Not transactional — see `SmrtJunction.setLinks` for caveats.
|
|
100
|
+
*/
|
|
101
|
+
// @ts-expect-error — diverges from SmrtJunction.setLinks(leftId, rightIds) by arity.
|
|
102
|
+
async setLinks(metaType, metaId, assetIds, opts = {}) {
|
|
103
|
+
const snapshotOpts = { ...opts };
|
|
104
|
+
delete snapshotOpts.assetId;
|
|
105
|
+
const existing = await this.list({
|
|
106
|
+
where: { ...snapshotOpts, metaType, metaId }
|
|
107
|
+
});
|
|
108
|
+
for (const link of existing) {
|
|
109
|
+
await link.delete();
|
|
110
|
+
}
|
|
111
|
+
const positionKey = this.positionField;
|
|
112
|
+
for (let i = 0; i < assetIds.length; i++) {
|
|
113
|
+
const rowOpts = { ...opts };
|
|
114
|
+
if (positionKey && rowOpts[positionKey] === void 0) {
|
|
115
|
+
rowOpts[positionKey] = i;
|
|
116
|
+
}
|
|
117
|
+
await this.attach(metaType, metaId, assetIds[i], rowOpts);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
__publicField(AssetAssociationCollection, "_itemClass", AssetAssociation);
|
|
122
|
+
AssetAssociationCollection = __decorateClass$5([
|
|
123
|
+
smrt()
|
|
124
|
+
], AssetAssociationCollection);
|
|
125
|
+
var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
|
|
126
|
+
var __decorateClass$4 = (decorators, target, key, kind) => {
|
|
127
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
|
|
128
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
129
|
+
if (decorator = decorators[i])
|
|
130
|
+
result = decorator(result) || result;
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
let AssetStatus = class extends SmrtObject {
|
|
134
|
+
// slug is inherited as an accessor from SmrtObject
|
|
135
|
+
name = "";
|
|
136
|
+
// Display name (e.g., 'Draft', 'Published')
|
|
137
|
+
description = "";
|
|
138
|
+
// Optional description
|
|
139
|
+
constructor(options = {}) {
|
|
140
|
+
super(options);
|
|
141
|
+
if (options.slug) this.slug = options.slug;
|
|
142
|
+
if (options.name) this.name = options.name;
|
|
143
|
+
if (options.description) this.description = options.description;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Get asset status by slug
|
|
147
|
+
*
|
|
148
|
+
* @param slug - The slug to search for
|
|
149
|
+
* @returns AssetStatus instance or null
|
|
150
|
+
*/
|
|
151
|
+
static async getBySlug(_slug) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
AssetStatus = __decorateClass$4([
|
|
156
|
+
smrt({
|
|
157
|
+
tableStrategy: "sti",
|
|
158
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
159
|
+
mcp: { include: ["list", "get", "create"] },
|
|
160
|
+
cli: true
|
|
161
|
+
})
|
|
162
|
+
], AssetStatus);
|
|
163
|
+
var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
|
|
164
|
+
var __decorateClass$3 = (decorators, target, key, kind) => {
|
|
165
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
|
|
166
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
167
|
+
if (decorator = decorators[i])
|
|
168
|
+
result = decorator(result) || result;
|
|
169
|
+
return result;
|
|
170
|
+
};
|
|
171
|
+
let AssetType = class extends SmrtObject {
|
|
172
|
+
// slug is inherited as an accessor from SmrtObject
|
|
173
|
+
name = "";
|
|
174
|
+
// Display name (e.g., 'Image', 'Video')
|
|
175
|
+
description = "";
|
|
176
|
+
// Optional description
|
|
177
|
+
constructor(options = {}) {
|
|
178
|
+
super(options);
|
|
179
|
+
if (options.slug) this.slug = options.slug;
|
|
180
|
+
if (options.name) this.name = options.name;
|
|
181
|
+
if (options.description) this.description = options.description;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get asset type by slug
|
|
185
|
+
*
|
|
186
|
+
* @param slug - The slug to search for
|
|
187
|
+
* @returns AssetType instance or null
|
|
188
|
+
*/
|
|
189
|
+
static async getBySlug(_slug) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
AssetType = __decorateClass$3([
|
|
194
|
+
smrt({
|
|
195
|
+
tableStrategy: "sti",
|
|
196
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
197
|
+
mcp: { include: ["list", "get", "create"] },
|
|
198
|
+
cli: true
|
|
199
|
+
})
|
|
200
|
+
], AssetType);
|
|
201
|
+
var __defProp$1 = Object.defineProperty;
|
|
202
|
+
var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
|
|
203
|
+
var __decorateClass$2 = (decorators, target, key, kind) => {
|
|
204
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
|
|
205
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
206
|
+
if (decorator = decorators[i])
|
|
207
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
208
|
+
if (kind && result) __defProp$1(target, key, result);
|
|
209
|
+
return result;
|
|
210
|
+
};
|
|
211
|
+
function parseAssetRecord(value) {
|
|
212
|
+
if (!value) return {};
|
|
213
|
+
if (typeof value !== "string") return value;
|
|
214
|
+
if (!value.trim()) return {};
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(value);
|
|
217
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
218
|
+
} catch {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function stringifyAssetRecord(value) {
|
|
223
|
+
if (!value || Object.keys(value).length === 0) return "";
|
|
224
|
+
return JSON.stringify(value);
|
|
225
|
+
}
|
|
226
|
+
let Asset = class extends SmrtObject {
|
|
227
|
+
tenantId = null;
|
|
228
|
+
// Core fields
|
|
229
|
+
name = "";
|
|
230
|
+
// User-friendly name
|
|
231
|
+
// slug is inherited as an accessor from SmrtObject
|
|
232
|
+
sourceUri = "";
|
|
233
|
+
// URI to the actual file (e.g., 's3://bucket/key', 'file:///path')
|
|
234
|
+
mimeType = "";
|
|
235
|
+
// MIME type (e.g., 'image/jpeg', 'video/mp4')
|
|
236
|
+
description = "";
|
|
237
|
+
// Optional description
|
|
238
|
+
metadata = "";
|
|
239
|
+
// JSON metadata owned by SMRT asset processors
|
|
240
|
+
version = 1;
|
|
241
|
+
primaryVersionId = null;
|
|
242
|
+
// Points to first version's ID
|
|
243
|
+
typeSlug = "";
|
|
244
|
+
// FK to AssetType.slug
|
|
245
|
+
statusSlug = "";
|
|
246
|
+
ownerProfileId = null;
|
|
247
|
+
sourceAssetId = null;
|
|
248
|
+
folderId = null;
|
|
249
|
+
// FK to Folder.id
|
|
250
|
+
// Provenance fields
|
|
251
|
+
sourceType = "";
|
|
252
|
+
// 'local', 'shutterstock', 'google-photos', 'upstream-smrt'
|
|
253
|
+
externalId = "";
|
|
254
|
+
// Original ID in upstream source
|
|
255
|
+
externalRefs = "";
|
|
256
|
+
// JSON map of provider references, keyed by provider slug
|
|
257
|
+
// Timestamps
|
|
258
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
259
|
+
updatedAt = /* @__PURE__ */ new Date();
|
|
260
|
+
constructor(options = {}) {
|
|
261
|
+
super(options);
|
|
262
|
+
if (options.name) this.name = options.name;
|
|
263
|
+
if (options.slug) this.slug = options.slug;
|
|
264
|
+
if (options.sourceUri) this.sourceUri = options.sourceUri;
|
|
265
|
+
if (options.mimeType) this.mimeType = options.mimeType;
|
|
266
|
+
if (options.description) this.description = options.description;
|
|
267
|
+
if (options.metadata !== void 0)
|
|
268
|
+
this.metadata = typeof options.metadata === "string" ? options.metadata : stringifyAssetRecord(options.metadata);
|
|
269
|
+
if (options.version !== void 0) this.version = options.version;
|
|
270
|
+
if (options.primaryVersionId !== void 0)
|
|
271
|
+
this.primaryVersionId = options.primaryVersionId;
|
|
272
|
+
if (options.typeSlug) this.typeSlug = options.typeSlug;
|
|
273
|
+
if (options.statusSlug) this.statusSlug = options.statusSlug;
|
|
274
|
+
if (options.ownerProfileId !== void 0)
|
|
275
|
+
this.ownerProfileId = options.ownerProfileId;
|
|
276
|
+
if (options.sourceAssetId !== void 0)
|
|
277
|
+
this.sourceAssetId = options.sourceAssetId;
|
|
278
|
+
if (options.folderId !== void 0) this.folderId = options.folderId;
|
|
279
|
+
if (options.sourceType) this.sourceType = options.sourceType;
|
|
280
|
+
if (options.externalId) this.externalId = options.externalId;
|
|
281
|
+
if (options.externalRefs !== void 0)
|
|
282
|
+
this.externalRefs = typeof options.externalRefs === "string" ? options.externalRefs : stringifyAssetRecord(options.externalRefs);
|
|
283
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
284
|
+
if (options.createdAt) this.createdAt = options.createdAt;
|
|
285
|
+
if (options.updatedAt) this.updatedAt = options.updatedAt;
|
|
286
|
+
}
|
|
287
|
+
getMetadata() {
|
|
288
|
+
return parseAssetRecord(this.metadata);
|
|
289
|
+
}
|
|
290
|
+
setMetadata(metadata) {
|
|
291
|
+
this.metadata = stringifyAssetRecord(metadata);
|
|
292
|
+
}
|
|
293
|
+
mergeMetadata(metadata) {
|
|
294
|
+
this.setMetadata({
|
|
295
|
+
...this.getMetadata(),
|
|
296
|
+
...metadata
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
getExternalRefs() {
|
|
300
|
+
const refs = parseAssetRecord(this.externalRefs);
|
|
301
|
+
const normalized = {};
|
|
302
|
+
for (const [provider, value] of Object.entries(refs)) {
|
|
303
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
304
|
+
normalized[provider] = {
|
|
305
|
+
...value,
|
|
306
|
+
provider
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return normalized;
|
|
311
|
+
}
|
|
312
|
+
getExternalRef(provider) {
|
|
313
|
+
return this.getExternalRefs()[provider] ?? null;
|
|
314
|
+
}
|
|
315
|
+
setExternalRef(provider, reference) {
|
|
316
|
+
this.externalRefs = stringifyAssetRecord({
|
|
317
|
+
...this.getExternalRefs(),
|
|
318
|
+
[provider]: {
|
|
319
|
+
...this.getExternalRef(provider) ?? {},
|
|
320
|
+
...reference,
|
|
321
|
+
provider: reference.provider ?? provider
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Get all tags for this asset from @happyvertical/smrt-tags
|
|
327
|
+
*
|
|
328
|
+
* @returns Array of Tag instances from @happyvertical/smrt-tags package
|
|
329
|
+
*/
|
|
330
|
+
async getTags() {
|
|
331
|
+
const db = this.db;
|
|
332
|
+
const rows = await db.list("asset_tags", {
|
|
333
|
+
where: { asset_id: this.id }
|
|
334
|
+
});
|
|
335
|
+
const tags = [];
|
|
336
|
+
for (const row of rows) {
|
|
337
|
+
const tag = await Tag.getBySlug(row.tag_slug);
|
|
338
|
+
if (tag) tags.push(tag);
|
|
339
|
+
}
|
|
340
|
+
return tags;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Check if this asset has a specific tag
|
|
344
|
+
*
|
|
345
|
+
* @param tagSlug - The slug of the tag to check
|
|
346
|
+
* @returns True if the asset has this tag
|
|
347
|
+
*/
|
|
348
|
+
async hasTag(tagSlug) {
|
|
349
|
+
const db = this.db;
|
|
350
|
+
const rows = await db.list("asset_tags", {
|
|
351
|
+
where: { asset_id: this.id, tag_slug: tagSlug }
|
|
352
|
+
});
|
|
353
|
+
return rows.length > 0;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Resolve the AssetCollection lazily. Going through `ObjectRegistry`
|
|
357
|
+
* mirrors the pattern used by `SmrtHierarchical._hierarchyCollection`
|
|
358
|
+
* so source/derivative lookups inherit tenant scoping and ORM
|
|
359
|
+
* hydration without hard-coding an import of `./assets` (which would
|
|
360
|
+
* create a module-import cycle).
|
|
361
|
+
*
|
|
362
|
+
* The return type is `SmrtCollection<Asset>` (the framework base, type-
|
|
363
|
+
* only import from core) rather than the concrete `AssetCollection` —
|
|
364
|
+
* importing the concrete class is what would create the cycle, but the
|
|
365
|
+
* base-class shape gives callers full `.get()` / `.list()` type
|
|
366
|
+
* safety here.
|
|
367
|
+
*
|
|
368
|
+
* R5-canon: hardcode the base Asset's qualified key so a different
|
|
369
|
+
* package also registering a class called `Asset` can't be picked
|
|
370
|
+
* by `findClass`'s multi-strategy fallback. Crucially we DON'T
|
|
371
|
+
* resolve via `this.constructor._smrtQualifiedName` — for an STI
|
|
372
|
+
* subclass like `Image`, that would yield the Image collection
|
|
373
|
+
* (which auto-filters `_meta_type = '...:Image'` on `get`/`list`),
|
|
374
|
+
* and `getSource()` / `getDerivatives()` would miss cross-type
|
|
375
|
+
* derivation links (Image derived from a plain Asset, etc.).
|
|
376
|
+
* `sourceAssetId` is a base-table derivation link, so it always
|
|
377
|
+
* resolves through the base Asset collection.
|
|
378
|
+
*/
|
|
379
|
+
async _assetCollection() {
|
|
380
|
+
return await ObjectRegistry.getCollection(
|
|
381
|
+
"@happyvertical/smrt-assets:Asset",
|
|
382
|
+
this.options
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get the source asset this one was derived from, if any.
|
|
387
|
+
*
|
|
388
|
+
* Renamed from `getParent` in R3-D. The relationship is "I was produced
|
|
389
|
+
* from that asset" (e.g. a thumbnail's source is its original image),
|
|
390
|
+
* not a structural-hierarchy parent.
|
|
391
|
+
*
|
|
392
|
+
* Goes through the AssetCollection so tenant interceptors and ORM
|
|
393
|
+
* hydration apply — important because a tenant-scoped consumer with
|
|
394
|
+
* cross-tenant derivative chains would otherwise return assets from
|
|
395
|
+
* tenants the caller cannot see, and a raw `db.get` returns
|
|
396
|
+
* snake_case rows that leave camelCase props (e.g. `sourceUri`) at
|
|
397
|
+
* their constructor defaults.
|
|
398
|
+
*
|
|
399
|
+
* @returns Source Asset instance, or null if this asset has no source
|
|
400
|
+
*/
|
|
401
|
+
async getSource() {
|
|
402
|
+
if (!this.sourceAssetId) return null;
|
|
403
|
+
const collection = await this._assetCollection();
|
|
404
|
+
return await collection.get({ id: this.sourceAssetId });
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Get all assets derived from this one (e.g. thumbnails, variants,
|
|
408
|
+
* transcodes, AI edits).
|
|
409
|
+
*
|
|
410
|
+
* Renamed from `getChildren` in R3-D to match the derivation
|
|
411
|
+
* semantics. Goes through the AssetCollection so tenant interceptors
|
|
412
|
+
* and ORM hydration apply (see `getSource` for why this matters —
|
|
413
|
+
* the pre-R3-D `getChildren` used raw `db.list`, which both bypassed
|
|
414
|
+
* tenant scoping and dropped camelCase property hydration; that
|
|
415
|
+
* latent breakage is fixed here).
|
|
416
|
+
*
|
|
417
|
+
* @returns Array of derivative Asset instances
|
|
418
|
+
*/
|
|
419
|
+
async getDerivatives() {
|
|
420
|
+
if (!this.id) return [];
|
|
421
|
+
const collection = await this._assetCollection();
|
|
422
|
+
return await collection.list({
|
|
423
|
+
where: { sourceAssetId: this.id }
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get the type of this asset
|
|
428
|
+
*
|
|
429
|
+
* @returns AssetType instance or null
|
|
430
|
+
*/
|
|
431
|
+
async getType() {
|
|
432
|
+
if (!this.typeSlug) return null;
|
|
433
|
+
return await AssetType.getBySlug(this.typeSlug);
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Get the status of this asset
|
|
437
|
+
*
|
|
438
|
+
* @returns AssetStatus instance or null
|
|
439
|
+
*/
|
|
440
|
+
async getStatus() {
|
|
441
|
+
if (!this.statusSlug) return null;
|
|
442
|
+
return await AssetStatus.getBySlug(this.statusSlug);
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Get all associations for this asset
|
|
446
|
+
*
|
|
447
|
+
* @returns Array of AssetAssociation instances
|
|
448
|
+
*/
|
|
449
|
+
async getAssociations() {
|
|
450
|
+
const associations = await AssetAssociationCollection.create({
|
|
451
|
+
db: this.db
|
|
452
|
+
});
|
|
453
|
+
return await associations.byRight(this.id);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Associate this asset with a target object
|
|
457
|
+
*
|
|
458
|
+
* @param metaType - Target class name or qualified name (e.g., 'Article' or '@pkg:Article')
|
|
459
|
+
* @param metaId - Target object ID
|
|
460
|
+
* @param role - Association role (default: 'default')
|
|
461
|
+
* @returns The created AssetAssociation
|
|
462
|
+
*/
|
|
463
|
+
async associateWith(metaType, metaId, role = "default") {
|
|
464
|
+
const associations = await AssetAssociationCollection.create({
|
|
465
|
+
db: this.db
|
|
466
|
+
});
|
|
467
|
+
return await associations.attach(metaType, metaId, this.id, { role });
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Get asset by slug
|
|
471
|
+
*
|
|
472
|
+
* @param slug - The slug to search for
|
|
473
|
+
* @returns Asset instance or null
|
|
474
|
+
*/
|
|
475
|
+
static async getBySlug(_slug) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
__decorateClass$2([
|
|
480
|
+
tenantId({ nullable: true })
|
|
481
|
+
], Asset.prototype, "tenantId", 2);
|
|
482
|
+
__decorateClass$2([
|
|
483
|
+
foreignKey("Asset")
|
|
484
|
+
], Asset.prototype, "primaryVersionId", 2);
|
|
485
|
+
__decorateClass$2([
|
|
486
|
+
crossPackageRef("@happyvertical/smrt-profiles:Profile")
|
|
487
|
+
], Asset.prototype, "ownerProfileId", 2);
|
|
488
|
+
__decorateClass$2([
|
|
489
|
+
foreignKey("Asset")
|
|
490
|
+
], Asset.prototype, "sourceAssetId", 2);
|
|
491
|
+
__decorateClass$2([
|
|
492
|
+
foreignKey("Folder")
|
|
493
|
+
], Asset.prototype, "folderId", 2);
|
|
494
|
+
Asset = __decorateClass$2([
|
|
495
|
+
TenantScoped({ mode: "optional" }),
|
|
496
|
+
smrt({
|
|
497
|
+
tableStrategy: "sti",
|
|
498
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
499
|
+
mcp: { include: ["list", "get", "create", "update"] },
|
|
500
|
+
cli: true
|
|
501
|
+
})
|
|
502
|
+
], Asset);
|
|
503
|
+
class AssetCapabilityUnavailableError extends Error {
|
|
504
|
+
constructor(capability, message = `No asset capability provider is registered for ${capability}.`) {
|
|
505
|
+
super(message);
|
|
506
|
+
this.capability = capability;
|
|
507
|
+
this.name = "AssetCapabilityUnavailableError";
|
|
508
|
+
}
|
|
509
|
+
capability;
|
|
510
|
+
}
|
|
511
|
+
class AssetCapabilitySkippedError extends Error {
|
|
512
|
+
constructor(capability, message) {
|
|
513
|
+
super(message);
|
|
514
|
+
this.capability = capability;
|
|
515
|
+
this.name = "AssetCapabilitySkippedError";
|
|
516
|
+
}
|
|
517
|
+
capability;
|
|
518
|
+
}
|
|
519
|
+
const ASSET_ROLES = {
|
|
520
|
+
SOURCE_DOCUMENT: "source_document",
|
|
521
|
+
DOCUMENT_IMAGE: "document_image",
|
|
522
|
+
THUMBNAIL: "thumbnail",
|
|
523
|
+
ASSET_VARIANT: "asset_variant",
|
|
524
|
+
PROOF: "proof",
|
|
525
|
+
DERIVATION_SOURCE: "derivation_source",
|
|
526
|
+
ATTACHMENT: "attachment",
|
|
527
|
+
HERO: "hero"
|
|
528
|
+
};
|
|
529
|
+
const ASSET_METADATA_KEYS = {
|
|
530
|
+
EXTRACTION_STATUS: "extractionStatus",
|
|
531
|
+
EXTRACTION_ERROR: "extractionError",
|
|
532
|
+
EXTRACTED_AT: "extractedAt",
|
|
533
|
+
SOURCE_URL: "sourceUrl",
|
|
534
|
+
SOURCE_HASH: "sourceHash",
|
|
535
|
+
PAGE_NUMBER: "pageNumber"
|
|
536
|
+
};
|
|
537
|
+
const ASSET_EXTRACTION_STATUS = {
|
|
538
|
+
PENDING: "pending",
|
|
539
|
+
RUNNING: "running",
|
|
540
|
+
SUCCEEDED: "succeeded",
|
|
541
|
+
FAILED: "failed"
|
|
542
|
+
};
|
|
543
|
+
var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
|
|
544
|
+
var __decorateClass$1 = (decorators, target, key, kind) => {
|
|
545
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
|
|
546
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
547
|
+
if (decorator = decorators[i])
|
|
548
|
+
result = decorator(result) || result;
|
|
549
|
+
return result;
|
|
550
|
+
};
|
|
551
|
+
let AssetMetafield = class extends SmrtObject {
|
|
552
|
+
// slug is inherited as an accessor from SmrtObject
|
|
553
|
+
name = "";
|
|
554
|
+
// Display name (e.g., 'Width', 'Height')
|
|
555
|
+
validation = "";
|
|
556
|
+
// JSON validation rules stored as text
|
|
557
|
+
constructor(options = {}) {
|
|
558
|
+
super(options);
|
|
559
|
+
if (options.slug) this.slug = options.slug;
|
|
560
|
+
if (options.name) this.name = options.name;
|
|
561
|
+
if (options.validation) this.validation = options.validation;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Get validation rules as parsed object
|
|
565
|
+
*
|
|
566
|
+
* @returns Parsed validation object or empty object if no validation
|
|
567
|
+
*/
|
|
568
|
+
getValidation() {
|
|
569
|
+
if (!this.validation) return {};
|
|
570
|
+
try {
|
|
571
|
+
return JSON.parse(this.validation);
|
|
572
|
+
} catch {
|
|
573
|
+
return {};
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Set validation rules from object
|
|
578
|
+
*
|
|
579
|
+
* @param rules - Validation rules object
|
|
580
|
+
*/
|
|
581
|
+
setValidation(rules) {
|
|
582
|
+
this.validation = JSON.stringify(rules);
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Get asset metafield by slug
|
|
586
|
+
*
|
|
587
|
+
* @param slug - The slug to search for
|
|
588
|
+
* @returns AssetMetafield instance or null
|
|
589
|
+
*/
|
|
590
|
+
static async getBySlug(_slug) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
AssetMetafield = __decorateClass$1([
|
|
595
|
+
smrt({
|
|
596
|
+
tableStrategy: "sti",
|
|
597
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
598
|
+
mcp: { include: ["list", "get", "create"] },
|
|
599
|
+
cli: true
|
|
600
|
+
})
|
|
601
|
+
], AssetMetafield);
|
|
602
|
+
class AssetMetafieldCollection extends SmrtCollection {
|
|
603
|
+
static _itemClass = AssetMetafield;
|
|
604
|
+
/**
|
|
605
|
+
* Get or create an asset metafield by slug
|
|
606
|
+
*
|
|
607
|
+
* @param slug - The metafield slug
|
|
608
|
+
* @param name - The display name (defaults to slug)
|
|
609
|
+
* @param validation - Optional validation rules (JSON string or object)
|
|
610
|
+
* @returns The existing or newly created AssetMetafield
|
|
611
|
+
*/
|
|
612
|
+
async getOrCreate(slug, name, validation) {
|
|
613
|
+
const existing = await this.list({ where: { slug }, limit: 1 });
|
|
614
|
+
if (existing.length > 0) {
|
|
615
|
+
return existing[0];
|
|
616
|
+
}
|
|
617
|
+
const validationString = typeof validation === "string" ? validation : validation ? JSON.stringify(validation) : "";
|
|
618
|
+
return await this.create({
|
|
619
|
+
slug,
|
|
620
|
+
name: name || slug,
|
|
621
|
+
validation: validationString
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* Initialize common asset metafields
|
|
626
|
+
*
|
|
627
|
+
* Creates standard metafields with validation rules:
|
|
628
|
+
* - width (integer, min: 0)
|
|
629
|
+
* - height (integer, min: 0)
|
|
630
|
+
* - duration (number, min: 0)
|
|
631
|
+
* - size (integer, min: 0)
|
|
632
|
+
* - author (string)
|
|
633
|
+
* - copyright (string)
|
|
634
|
+
*/
|
|
635
|
+
async initializeCommonMetafields() {
|
|
636
|
+
await this.getOrCreate("width", "Width", {
|
|
637
|
+
type: "integer",
|
|
638
|
+
min: 0,
|
|
639
|
+
description: "Width in pixels"
|
|
640
|
+
});
|
|
641
|
+
await this.getOrCreate("height", "Height", {
|
|
642
|
+
type: "integer",
|
|
643
|
+
min: 0,
|
|
644
|
+
description: "Height in pixels"
|
|
645
|
+
});
|
|
646
|
+
await this.getOrCreate("duration", "Duration", {
|
|
647
|
+
type: "number",
|
|
648
|
+
min: 0,
|
|
649
|
+
description: "Duration in seconds"
|
|
650
|
+
});
|
|
651
|
+
await this.getOrCreate("size", "File Size", {
|
|
652
|
+
type: "integer",
|
|
653
|
+
min: 0,
|
|
654
|
+
description: "File size in bytes"
|
|
655
|
+
});
|
|
656
|
+
await this.getOrCreate("author", "Author", {
|
|
657
|
+
type: "string",
|
|
658
|
+
description: "Content creator"
|
|
659
|
+
});
|
|
660
|
+
await this.getOrCreate("copyright", "Copyright", {
|
|
661
|
+
type: "string",
|
|
662
|
+
description: "Copyright notice"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const MIME_TO_EXT = {
|
|
667
|
+
"image/png": "png",
|
|
668
|
+
"image/jpeg": "jpg",
|
|
669
|
+
"image/webp": "webp",
|
|
670
|
+
"image/gif": "gif",
|
|
671
|
+
"image/svg+xml": "svg",
|
|
672
|
+
"video/mp4": "mp4",
|
|
673
|
+
"video/webm": "webm",
|
|
674
|
+
"audio/wav": "wav",
|
|
675
|
+
"audio/mpeg": "mp3",
|
|
676
|
+
"audio/ogg": "ogg",
|
|
677
|
+
"audio/flac": "flac",
|
|
678
|
+
"application/pdf": "pdf",
|
|
679
|
+
"application/json": "json",
|
|
680
|
+
"text/plain": "txt"
|
|
681
|
+
};
|
|
682
|
+
function serializeStoreMetadata(metadata) {
|
|
683
|
+
if (metadata === void 0) return void 0;
|
|
684
|
+
if (metadata === null) return "";
|
|
685
|
+
if (typeof metadata === "string") return metadata;
|
|
686
|
+
return JSON.stringify(metadata);
|
|
687
|
+
}
|
|
688
|
+
function normalizeProviderOptions(providerOrBasePath) {
|
|
689
|
+
if (typeof providerOrBasePath === "string") {
|
|
690
|
+
return { type: "local", basePath: providerOrBasePath };
|
|
691
|
+
}
|
|
692
|
+
return providerOrBasePath;
|
|
693
|
+
}
|
|
694
|
+
class AssetStore {
|
|
695
|
+
constructor(providerOrBasePath, collection, options = {}) {
|
|
696
|
+
this.collection = collection;
|
|
697
|
+
this.fsOptions = normalizeProviderOptions(providerOrBasePath);
|
|
698
|
+
this.resolver = options.resolver;
|
|
699
|
+
}
|
|
700
|
+
collection;
|
|
701
|
+
fs = null;
|
|
702
|
+
fsOptions;
|
|
703
|
+
fsCache = /* @__PURE__ */ new Map();
|
|
704
|
+
resolver;
|
|
705
|
+
/** The base path for local storage, or empty string for non-local providers */
|
|
706
|
+
get basePath() {
|
|
707
|
+
return this.fsOptions.basePath ?? "";
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Initialize the filesystem adapter.
|
|
711
|
+
* Must be called before any file operations.
|
|
712
|
+
*/
|
|
713
|
+
async initialize() {
|
|
714
|
+
this.fs = await getFilesystem(this.fsOptions);
|
|
715
|
+
this.fsCache.set(this.providerCacheKey(this.fsOptions), this.fs);
|
|
716
|
+
return this;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Get the initialized filesystem (throws if not initialized)
|
|
720
|
+
*/
|
|
721
|
+
getFs() {
|
|
722
|
+
if (!this.fs) {
|
|
723
|
+
throw new Error("AssetStore not initialized. Call initialize() first.");
|
|
724
|
+
}
|
|
725
|
+
return this.fs;
|
|
726
|
+
}
|
|
727
|
+
providerCacheKey(providerOptions) {
|
|
728
|
+
return JSON.stringify(providerOptions);
|
|
729
|
+
}
|
|
730
|
+
async getFilesystemForOptions(providerOptions) {
|
|
731
|
+
const key = this.providerCacheKey(providerOptions);
|
|
732
|
+
const cached = this.fsCache.get(key);
|
|
733
|
+
if (cached) return cached;
|
|
734
|
+
const filesystem = await getFilesystem(providerOptions);
|
|
735
|
+
this.fsCache.set(key, filesystem);
|
|
736
|
+
return filesystem;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Build a sourceUri for the given file path based on the provider type.
|
|
740
|
+
*/
|
|
741
|
+
buildSourceUri(filePath) {
|
|
742
|
+
return AssetStore.buildSourceUriForProvider(filePath, this.fsOptions);
|
|
743
|
+
}
|
|
744
|
+
static buildSourceUriForProvider(filePath, providerOptions) {
|
|
745
|
+
const type = providerOptions.type;
|
|
746
|
+
if (type === "s3") {
|
|
747
|
+
const bucket = providerOptions.bucket ?? "";
|
|
748
|
+
return `s3://${bucket}/${filePath}`;
|
|
749
|
+
}
|
|
750
|
+
const base = providerOptions.basePath ?? "";
|
|
751
|
+
return base ? `file://${base}/${filePath}` : `file://${filePath}`;
|
|
752
|
+
}
|
|
753
|
+
static providerRelativePath(filePath, providerOptions) {
|
|
754
|
+
const base = providerOptions.basePath ?? "";
|
|
755
|
+
return base && filePath.startsWith(base) ? filePath.slice(base.length + 1) : filePath;
|
|
756
|
+
}
|
|
757
|
+
static requireAssetId(asset) {
|
|
758
|
+
if (!asset.id) {
|
|
759
|
+
throw new Error("Asset must be saved before storing file data.");
|
|
760
|
+
}
|
|
761
|
+
return asset.id;
|
|
762
|
+
}
|
|
763
|
+
async resolveStorage(request) {
|
|
764
|
+
const defaultFilesystem = this.getFs();
|
|
765
|
+
const resolution = this.resolver ? await this.resolver({
|
|
766
|
+
...request,
|
|
767
|
+
defaultProviderOptions: this.fsOptions,
|
|
768
|
+
defaultFilesystem
|
|
769
|
+
}) : void 0;
|
|
770
|
+
if (request.operation === "write" && resolution?.filesystem && !resolution.providerOptions && !resolution.sourceUri) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
"Asset storage resolver must return providerOptions or sourceUri when overriding filesystem for write operations."
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
const providerOptions = resolution?.providerOptions ? normalizeProviderOptions(resolution.providerOptions) : this.fsOptions;
|
|
776
|
+
const path = AssetStore.providerRelativePath(
|
|
777
|
+
resolution?.path ?? request.path,
|
|
778
|
+
providerOptions
|
|
779
|
+
);
|
|
780
|
+
const sourceUri = resolution?.sourceUri ?? (resolution?.path || resolution?.providerOptions ? AssetStore.buildSourceUriForProvider(path, providerOptions) : request.sourceUri);
|
|
781
|
+
const filesystem = resolution?.filesystem ?? (providerOptions === this.fsOptions ? defaultFilesystem : await this.getFilesystemForOptions(providerOptions));
|
|
782
|
+
return {
|
|
783
|
+
filesystem,
|
|
784
|
+
providerOptions,
|
|
785
|
+
path,
|
|
786
|
+
sourceUri
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
async writeAssetData(asset, data, opts) {
|
|
790
|
+
const ext = MIME_TO_EXT[opts.mimeType] ?? "bin";
|
|
791
|
+
const typeSlug = opts.typeSlug ?? "file";
|
|
792
|
+
const assetId = AssetStore.requireAssetId(asset);
|
|
793
|
+
const filePath = `${typeSlug}/${assetId}.${ext}`;
|
|
794
|
+
const sourceUri = this.buildSourceUri(filePath);
|
|
795
|
+
const target = await this.resolveStorage({
|
|
796
|
+
operation: "write",
|
|
797
|
+
asset,
|
|
798
|
+
path: filePath,
|
|
799
|
+
sourceUri,
|
|
800
|
+
mimeType: opts.mimeType,
|
|
801
|
+
typeSlug
|
|
802
|
+
});
|
|
803
|
+
await target.filesystem.write(target.path, data, { createParents: true });
|
|
804
|
+
return target.sourceUri;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Write file data for an existing Asset record (no DB record created).
|
|
808
|
+
*
|
|
809
|
+
* Use this when you've already created the record (e.g., via a
|
|
810
|
+
* collection.create()) and only need to persist the file data.
|
|
811
|
+
*
|
|
812
|
+
* @param asset - The existing asset to write data for
|
|
813
|
+
* @param data - File data as a Buffer
|
|
814
|
+
* @param opts - Storage options (mimeType required)
|
|
815
|
+
* @returns The sourceUri for the written file
|
|
816
|
+
*/
|
|
817
|
+
async storeFile(asset, data, opts) {
|
|
818
|
+
return this.writeAssetData(asset, data, opts);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Write buffer to disk and create an Asset record.
|
|
822
|
+
*
|
|
823
|
+
* @param name - Human-readable name for the asset
|
|
824
|
+
* @param data - File data as a Buffer
|
|
825
|
+
* @param opts - Storage options (mimeType required)
|
|
826
|
+
* @returns Created Asset instance
|
|
827
|
+
*/
|
|
828
|
+
async store(name, data, opts) {
|
|
829
|
+
const typeSlug = opts.typeSlug ?? "file";
|
|
830
|
+
const asset = await this.collection.create({
|
|
831
|
+
name,
|
|
832
|
+
mimeType: opts.mimeType,
|
|
833
|
+
typeSlug,
|
|
834
|
+
statusSlug: opts.statusSlug ?? "active",
|
|
835
|
+
sourceAssetId: opts.sourceAssetId ?? null,
|
|
836
|
+
description: opts.description ?? "",
|
|
837
|
+
sourceType: opts.sourceType ?? "",
|
|
838
|
+
externalId: opts.externalId ?? "",
|
|
839
|
+
metadata: serializeStoreMetadata(opts.metadata) ?? "",
|
|
840
|
+
sourceUri: ""
|
|
841
|
+
// Will be updated after file write
|
|
842
|
+
});
|
|
843
|
+
try {
|
|
844
|
+
asset.sourceUri = await this.writeAssetData(asset, data, {
|
|
845
|
+
mimeType: opts.mimeType,
|
|
846
|
+
typeSlug
|
|
847
|
+
});
|
|
848
|
+
} catch (err) {
|
|
849
|
+
await asset.delete();
|
|
850
|
+
throw err;
|
|
851
|
+
}
|
|
852
|
+
await asset.save();
|
|
853
|
+
return asset;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Store a new version of an existing asset.
|
|
857
|
+
*
|
|
858
|
+
* @param asset - The existing asset to version
|
|
859
|
+
* @param data - File data for the new version
|
|
860
|
+
* @param opts - Optional overrides for store options
|
|
861
|
+
* @returns The newly created version Asset
|
|
862
|
+
*/
|
|
863
|
+
async storeVersion(asset, data, opts = {}) {
|
|
864
|
+
const primaryVersionId = asset.primaryVersionId ?? AssetStore.requireAssetId(asset);
|
|
865
|
+
const { metadata, ...versionOptions } = opts;
|
|
866
|
+
const versionUpdates = { ...versionOptions };
|
|
867
|
+
const serializedMetadata = serializeStoreMetadata(metadata);
|
|
868
|
+
if (serializedMetadata !== void 0) {
|
|
869
|
+
versionUpdates.metadata = serializedMetadata;
|
|
870
|
+
} else {
|
|
871
|
+
delete versionUpdates.metadata;
|
|
872
|
+
}
|
|
873
|
+
const newVersion = await this.collection.createNewVersion(
|
|
874
|
+
primaryVersionId,
|
|
875
|
+
"",
|
|
876
|
+
// sourceUri will be set by store
|
|
877
|
+
versionUpdates
|
|
878
|
+
);
|
|
879
|
+
const mimeType = opts.mimeType ?? asset.mimeType;
|
|
880
|
+
const typeSlug = opts.typeSlug ?? asset.typeSlug ?? "file";
|
|
881
|
+
try {
|
|
882
|
+
newVersion.sourceUri = await this.writeAssetData(newVersion, data, {
|
|
883
|
+
mimeType,
|
|
884
|
+
typeSlug
|
|
885
|
+
});
|
|
886
|
+
} catch (err) {
|
|
887
|
+
await newVersion.delete();
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
if (opts.mimeType) newVersion.mimeType = opts.mimeType;
|
|
891
|
+
await newVersion.save();
|
|
892
|
+
return newVersion;
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Read file data for a specific version of an asset.
|
|
896
|
+
*
|
|
897
|
+
* @param asset - The asset (any version in the chain)
|
|
898
|
+
* @param version - The version number to read
|
|
899
|
+
* @returns File data as a Buffer
|
|
900
|
+
*/
|
|
901
|
+
async readVersion(asset, version) {
|
|
902
|
+
const primaryVersionId = asset.primaryVersionId ?? AssetStore.requireAssetId(asset);
|
|
903
|
+
const versions = await this.collection.listVersions(primaryVersionId);
|
|
904
|
+
const targetVersion = versions.find((v) => v.version === version);
|
|
905
|
+
if (!targetVersion) {
|
|
906
|
+
throw new Error(
|
|
907
|
+
`Version ${version} not found for asset ${primaryVersionId}`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
return this.read(targetVersion);
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Read file data from an Asset's sourceUri.
|
|
914
|
+
*
|
|
915
|
+
* @param asset - Asset to read data for
|
|
916
|
+
* @returns File data as a Buffer
|
|
917
|
+
*/
|
|
918
|
+
async read(asset) {
|
|
919
|
+
const filePath = AssetStore.pathFromUri(asset.sourceUri);
|
|
920
|
+
const target = await this.resolveStorage({
|
|
921
|
+
operation: "read",
|
|
922
|
+
asset,
|
|
923
|
+
path: AssetStore.providerRelativePath(filePath, this.fsOptions),
|
|
924
|
+
sourceUri: asset.sourceUri
|
|
925
|
+
});
|
|
926
|
+
return await target.filesystem.read(target.path, { raw: true });
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Read file by asset ID.
|
|
930
|
+
*
|
|
931
|
+
* @param id - Asset ID to look up
|
|
932
|
+
* @returns Object with data Buffer and Asset, or null if not found
|
|
933
|
+
*/
|
|
934
|
+
async readById(id) {
|
|
935
|
+
const asset = await this.collection.get({ id });
|
|
936
|
+
if (!asset) return null;
|
|
937
|
+
try {
|
|
938
|
+
const data = await this.read(asset);
|
|
939
|
+
return { data, asset };
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Delete file from disk and remove the Asset record.
|
|
946
|
+
*
|
|
947
|
+
* @param asset - Asset to remove
|
|
948
|
+
*/
|
|
949
|
+
async remove(asset) {
|
|
950
|
+
if (asset.sourceUri) {
|
|
951
|
+
const filePath = AssetStore.pathFromUri(asset.sourceUri);
|
|
952
|
+
const target = await this.resolveStorage({
|
|
953
|
+
operation: "delete",
|
|
954
|
+
asset,
|
|
955
|
+
path: AssetStore.providerRelativePath(filePath, this.fsOptions),
|
|
956
|
+
sourceUri: asset.sourceUri
|
|
957
|
+
});
|
|
958
|
+
try {
|
|
959
|
+
await target.filesystem.delete(target.path);
|
|
960
|
+
} catch (err) {
|
|
961
|
+
if (!(err instanceof FileNotFoundError)) {
|
|
962
|
+
throw err;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
await asset.delete();
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* Extract filesystem path from a sourceUri.
|
|
970
|
+
* Handles file://, s3://, and plain paths.
|
|
971
|
+
*
|
|
972
|
+
* @param sourceUri - Asset sourceUri (e.g., 'file:///path/to/file.mp4', 's3://bucket/key')
|
|
973
|
+
* @returns Filesystem path
|
|
974
|
+
*/
|
|
975
|
+
static pathFromUri(sourceUri) {
|
|
976
|
+
if (sourceUri.startsWith("file://")) {
|
|
977
|
+
return sourceUri.slice(7);
|
|
978
|
+
}
|
|
979
|
+
if (sourceUri.startsWith("s3://")) {
|
|
980
|
+
const withoutScheme = sourceUri.slice(5);
|
|
981
|
+
const slashIdx = withoutScheme.indexOf("/");
|
|
982
|
+
return slashIdx >= 0 ? withoutScheme.slice(slashIdx + 1) : withoutScheme;
|
|
983
|
+
}
|
|
984
|
+
return sourceUri;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
class AssetCollection extends SmrtCollection {
|
|
988
|
+
static _itemClass = Asset;
|
|
989
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
990
|
+
// Tenant-Aware Query Methods
|
|
991
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
992
|
+
/**
|
|
993
|
+
* Find all assets belonging to a specific tenant
|
|
994
|
+
*
|
|
995
|
+
* @param tenantId - The tenant ID to filter by
|
|
996
|
+
* @returns Array of assets belonging to this tenant
|
|
997
|
+
*/
|
|
998
|
+
async findByTenant(tenantId2) {
|
|
999
|
+
return await this.list({ where: { tenantId: tenantId2 } });
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Find all global assets (assets without a tenant)
|
|
1003
|
+
*
|
|
1004
|
+
* @returns Array of global assets
|
|
1005
|
+
*/
|
|
1006
|
+
async findGlobal() {
|
|
1007
|
+
return await this.list({ where: { tenantId: null } });
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Find assets belonging to a tenant plus all global assets
|
|
1011
|
+
*
|
|
1012
|
+
* @param tenantId - The tenant ID to include
|
|
1013
|
+
* @returns Array of tenant-specific and global assets
|
|
1014
|
+
*/
|
|
1015
|
+
async findWithGlobals(tenantId2) {
|
|
1016
|
+
return await this.query(
|
|
1017
|
+
`SELECT * FROM ${this.tableName} WHERE tenant_id = ? OR tenant_id IS NULL`,
|
|
1018
|
+
[tenantId2]
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Add a tag to an asset (uses @smrt/tags)
|
|
1023
|
+
*
|
|
1024
|
+
* @param assetId - The asset ID to tag
|
|
1025
|
+
* @param tagSlug - The tag slug from @smrt/tags
|
|
1026
|
+
*/
|
|
1027
|
+
async addTag(assetId, tagSlug) {
|
|
1028
|
+
const db = this.db;
|
|
1029
|
+
await db.upsert("asset_tags", ["asset_id", "tag_slug"], {
|
|
1030
|
+
asset_id: assetId,
|
|
1031
|
+
tag_slug: tagSlug,
|
|
1032
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Remove a tag from an asset
|
|
1037
|
+
*
|
|
1038
|
+
* @param assetId - The asset ID
|
|
1039
|
+
* @param tagSlug - The tag slug to remove
|
|
1040
|
+
*/
|
|
1041
|
+
async removeTag(assetId, tagSlug) {
|
|
1042
|
+
const db = this.db;
|
|
1043
|
+
await db.delete("asset_tags", {
|
|
1044
|
+
asset_id: assetId,
|
|
1045
|
+
tag_slug: tagSlug
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Get all assets with a specific tag
|
|
1050
|
+
*
|
|
1051
|
+
* @param tagSlug - The tag slug to filter by
|
|
1052
|
+
* @returns Array of assets with this tag
|
|
1053
|
+
*/
|
|
1054
|
+
async getByTag(tagSlug) {
|
|
1055
|
+
const db = this.db;
|
|
1056
|
+
const rows = await db.list("asset_tags", { tag_slug: tagSlug });
|
|
1057
|
+
const assets = [];
|
|
1058
|
+
for (const row of rows) {
|
|
1059
|
+
const asset = await this.get({ id: row.asset_id });
|
|
1060
|
+
if (asset) assets.push(asset);
|
|
1061
|
+
}
|
|
1062
|
+
return assets;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Get assets by type
|
|
1066
|
+
*
|
|
1067
|
+
* @param typeSlug - The asset type slug (e.g., 'image', 'video')
|
|
1068
|
+
* @returns Array of assets matching the type
|
|
1069
|
+
*/
|
|
1070
|
+
async getByType(typeSlug) {
|
|
1071
|
+
return await this.list({ where: { typeSlug } });
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get assets by status
|
|
1075
|
+
*
|
|
1076
|
+
* @param statusSlug - The asset status slug (e.g., 'published', 'draft')
|
|
1077
|
+
* @returns Array of assets matching the status
|
|
1078
|
+
*/
|
|
1079
|
+
async getByStatus(statusSlug) {
|
|
1080
|
+
return await this.list({ where: { statusSlug } });
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Get assets by owner
|
|
1084
|
+
*
|
|
1085
|
+
* @param ownerProfileId - The profile ID of the owner
|
|
1086
|
+
* @returns Array of assets owned by this profile
|
|
1087
|
+
*/
|
|
1088
|
+
async getByOwner(ownerProfileId) {
|
|
1089
|
+
return await this.list({ where: { ownerProfileId } });
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Create a new version of an existing asset
|
|
1093
|
+
*
|
|
1094
|
+
* @param primaryVersionId - The primary version ID (first version's ID)
|
|
1095
|
+
* @param newSourceUri - The new source URI for this version
|
|
1096
|
+
* @param updates - Optional additional updates
|
|
1097
|
+
* @returns The newly created asset version
|
|
1098
|
+
*/
|
|
1099
|
+
async createNewVersion(primaryVersionId, newSourceUri, updates = {}) {
|
|
1100
|
+
const versions = await this.listVersions(primaryVersionId);
|
|
1101
|
+
if (versions.length === 0) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`No asset found with primary version ID: ${primaryVersionId}`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
versions.sort((a, b) => b.version - a.version);
|
|
1107
|
+
const latestVersion = versions[0];
|
|
1108
|
+
const newVersionNumber = latestVersion.version + 1;
|
|
1109
|
+
const primary = versions.find((v) => v.id === primaryVersionId) ?? versions[versions.length - 1];
|
|
1110
|
+
const baseSlug = String(primary?.slug ?? "") || "asset";
|
|
1111
|
+
const takenInChain = new Set(
|
|
1112
|
+
versions.map((v) => v.slug).filter(Boolean)
|
|
1113
|
+
);
|
|
1114
|
+
let candidate = `${baseSlug}-v${newVersionNumber}`;
|
|
1115
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
1116
|
+
const collides = takenInChain.has(candidate) || await this.get({ slug: candidate }) !== null;
|
|
1117
|
+
if (!collides) break;
|
|
1118
|
+
candidate = `${baseSlug}-v${newVersionNumber}-${attempt}`;
|
|
1119
|
+
}
|
|
1120
|
+
return await this.create({
|
|
1121
|
+
...latestVersion,
|
|
1122
|
+
id: void 0,
|
|
1123
|
+
// Generate new ID
|
|
1124
|
+
slug: candidate,
|
|
1125
|
+
sourceUri: newSourceUri,
|
|
1126
|
+
version: newVersionNumber,
|
|
1127
|
+
primaryVersionId,
|
|
1128
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1129
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
1130
|
+
...updates
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Get the latest version of an asset
|
|
1135
|
+
*
|
|
1136
|
+
* @param primaryVersionId - The primary version ID
|
|
1137
|
+
* @returns The latest version or null
|
|
1138
|
+
*/
|
|
1139
|
+
async getLatestVersion(primaryVersionId) {
|
|
1140
|
+
const versions = await this.listVersions(primaryVersionId);
|
|
1141
|
+
if (versions.length === 0) return null;
|
|
1142
|
+
versions.sort((a, b) => b.version - a.version);
|
|
1143
|
+
return versions[0];
|
|
1144
|
+
}
|
|
1145
|
+
/**
|
|
1146
|
+
* List all versions of an asset
|
|
1147
|
+
*
|
|
1148
|
+
* @param primaryVersionId - The primary version ID
|
|
1149
|
+
* @returns Array of all asset versions, ordered by version number
|
|
1150
|
+
*/
|
|
1151
|
+
async listVersions(primaryVersionId) {
|
|
1152
|
+
const [chained, primary] = await Promise.all([
|
|
1153
|
+
this.list({ where: { primaryVersionId } }),
|
|
1154
|
+
this.get({ id: primaryVersionId })
|
|
1155
|
+
]);
|
|
1156
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1157
|
+
for (const asset of [...primary ? [primary] : [], ...chained]) {
|
|
1158
|
+
if (asset?.id) byId.set(asset.id, asset);
|
|
1159
|
+
}
|
|
1160
|
+
const assets = Array.from(byId.values());
|
|
1161
|
+
assets.sort((a, b) => a.version - b.version);
|
|
1162
|
+
return assets;
|
|
1163
|
+
}
|
|
1164
|
+
/**
|
|
1165
|
+
* Get derivative assets of a source asset.
|
|
1166
|
+
*
|
|
1167
|
+
* Renamed from `getChildren(parentId)` in R3-D to match the rename of
|
|
1168
|
+
* the underlying column (`parent_id` → `source_asset_id`) and method
|
|
1169
|
+
* (`Asset.getChildren` → `Asset.getDerivatives`).
|
|
1170
|
+
*
|
|
1171
|
+
* @param sourceAssetId - The source asset ID
|
|
1172
|
+
* @returns Array of derivative assets
|
|
1173
|
+
*/
|
|
1174
|
+
async getDerivatives(sourceAssetId) {
|
|
1175
|
+
return await this.list({ where: { sourceAssetId } });
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Get assets by MIME type pattern
|
|
1179
|
+
*
|
|
1180
|
+
* @param mimePattern - MIME type pattern (e.g., 'image/*', 'video/mp4')
|
|
1181
|
+
* @returns Array of matching assets
|
|
1182
|
+
*/
|
|
1183
|
+
async getByMimeType(mimePattern) {
|
|
1184
|
+
const pattern = mimePattern.replace("*", "%");
|
|
1185
|
+
return await this.list({
|
|
1186
|
+
where: { "mimeType like": pattern }
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Rollback to a previous version by creating a new version with the target's content.
|
|
1191
|
+
* Does NOT delete intermediate versions (safe rollback).
|
|
1192
|
+
*
|
|
1193
|
+
* @param primaryVersionId - The primary version ID of the version chain
|
|
1194
|
+
* @param targetVersion - The version number to rollback to
|
|
1195
|
+
* @returns The newly created asset version with content copied from target
|
|
1196
|
+
*/
|
|
1197
|
+
async rollbackToVersion(primaryVersionId, targetVersion) {
|
|
1198
|
+
const versions = await this.listVersions(primaryVersionId);
|
|
1199
|
+
const target = versions.find((v) => v.version === targetVersion);
|
|
1200
|
+
if (!target) {
|
|
1201
|
+
throw new Error(
|
|
1202
|
+
`Version ${targetVersion} not found for asset ${primaryVersionId}`
|
|
1203
|
+
);
|
|
1204
|
+
}
|
|
1205
|
+
return await this.createNewVersion(primaryVersionId, target.sourceUri, {
|
|
1206
|
+
description: `Rollback to version ${targetVersion}`
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get assets in a specific folder
|
|
1211
|
+
*
|
|
1212
|
+
* @param folderId - The folder ID to list contents for
|
|
1213
|
+
* @returns Array of assets in this folder
|
|
1214
|
+
*/
|
|
1215
|
+
async getByFolder(folderId) {
|
|
1216
|
+
return await this.list({ where: { folderId } });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
class AssetRuntime {
|
|
1220
|
+
constructor(collection, associations, store, capabilityProviders = []) {
|
|
1221
|
+
this.collection = collection;
|
|
1222
|
+
this.associations = associations;
|
|
1223
|
+
this.store = store;
|
|
1224
|
+
this.capabilityProviders = [...capabilityProviders];
|
|
1225
|
+
}
|
|
1226
|
+
collection;
|
|
1227
|
+
associations;
|
|
1228
|
+
store;
|
|
1229
|
+
capabilityProviders;
|
|
1230
|
+
registerCapabilityProvider(provider) {
|
|
1231
|
+
this.capabilityProviders.push(provider);
|
|
1232
|
+
return this;
|
|
1233
|
+
}
|
|
1234
|
+
providersFor(capability) {
|
|
1235
|
+
const providers = this.capabilityProviders.filter(
|
|
1236
|
+
(candidate) => typeof candidate[capability] === "function"
|
|
1237
|
+
);
|
|
1238
|
+
if (providers.length === 0) {
|
|
1239
|
+
throw new AssetCapabilityUnavailableError(capability);
|
|
1240
|
+
}
|
|
1241
|
+
return providers;
|
|
1242
|
+
}
|
|
1243
|
+
async processAsset(asset, input = {}) {
|
|
1244
|
+
let skipped = null;
|
|
1245
|
+
for (const provider of this.providersFor("processAsset")) {
|
|
1246
|
+
const processAsset = provider.processAsset;
|
|
1247
|
+
if (!processAsset) continue;
|
|
1248
|
+
try {
|
|
1249
|
+
return await processAsset({
|
|
1250
|
+
runtime: this,
|
|
1251
|
+
asset,
|
|
1252
|
+
...input
|
|
1253
|
+
});
|
|
1254
|
+
} catch (cause) {
|
|
1255
|
+
if (cause instanceof AssetCapabilitySkippedError) {
|
|
1256
|
+
skipped = cause;
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
throw cause;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
throw new AssetCapabilityUnavailableError("processAsset", skipped?.message);
|
|
1263
|
+
}
|
|
1264
|
+
async ensureVariant(asset, request) {
|
|
1265
|
+
let skipped = null;
|
|
1266
|
+
for (const provider of this.providersFor("ensureVariant")) {
|
|
1267
|
+
const ensureVariant = provider.ensureVariant;
|
|
1268
|
+
if (!ensureVariant) continue;
|
|
1269
|
+
try {
|
|
1270
|
+
return await ensureVariant({
|
|
1271
|
+
runtime: this,
|
|
1272
|
+
asset,
|
|
1273
|
+
request
|
|
1274
|
+
});
|
|
1275
|
+
} catch (cause) {
|
|
1276
|
+
if (cause instanceof AssetCapabilitySkippedError) {
|
|
1277
|
+
skipped = cause;
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
throw cause;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
throw new AssetCapabilityUnavailableError(
|
|
1284
|
+
"ensureVariant",
|
|
1285
|
+
skipped?.message
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
async searchNearbyAssets(input) {
|
|
1289
|
+
let skipped = null;
|
|
1290
|
+
for (const provider of this.providersFor("searchNearbyAssets")) {
|
|
1291
|
+
const searchNearbyAssets = provider.searchNearbyAssets;
|
|
1292
|
+
if (!searchNearbyAssets) continue;
|
|
1293
|
+
try {
|
|
1294
|
+
return await searchNearbyAssets({
|
|
1295
|
+
runtime: this,
|
|
1296
|
+
...input
|
|
1297
|
+
});
|
|
1298
|
+
} catch (cause) {
|
|
1299
|
+
if (cause instanceof AssetCapabilitySkippedError) {
|
|
1300
|
+
skipped = cause;
|
|
1301
|
+
continue;
|
|
1302
|
+
}
|
|
1303
|
+
throw cause;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
throw new AssetCapabilityUnavailableError(
|
|
1307
|
+
"searchNearbyAssets",
|
|
1308
|
+
skipped?.message
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
async syncExternalAsset(asset, input = {}) {
|
|
1312
|
+
let skipped = null;
|
|
1313
|
+
for (const provider of this.providersFor("syncExternalAsset")) {
|
|
1314
|
+
const syncExternalAsset = provider.syncExternalAsset;
|
|
1315
|
+
if (!syncExternalAsset) continue;
|
|
1316
|
+
try {
|
|
1317
|
+
return await syncExternalAsset({
|
|
1318
|
+
runtime: this,
|
|
1319
|
+
asset,
|
|
1320
|
+
...input
|
|
1321
|
+
});
|
|
1322
|
+
} catch (cause) {
|
|
1323
|
+
if (cause instanceof AssetCapabilitySkippedError) {
|
|
1324
|
+
skipped = cause;
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
throw cause;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
throw new AssetCapabilityUnavailableError(
|
|
1331
|
+
"syncExternalAsset",
|
|
1332
|
+
skipped?.message
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
async submitAssetWorkflow(asset, input) {
|
|
1336
|
+
let skipped = null;
|
|
1337
|
+
for (const provider of this.providersFor("submitAssetWorkflow")) {
|
|
1338
|
+
const submitAssetWorkflow = provider.submitAssetWorkflow;
|
|
1339
|
+
if (!submitAssetWorkflow) continue;
|
|
1340
|
+
try {
|
|
1341
|
+
return await submitAssetWorkflow({
|
|
1342
|
+
runtime: this,
|
|
1343
|
+
asset,
|
|
1344
|
+
...input
|
|
1345
|
+
});
|
|
1346
|
+
} catch (cause) {
|
|
1347
|
+
if (cause instanceof AssetCapabilitySkippedError) {
|
|
1348
|
+
skipped = cause;
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
throw cause;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
throw new AssetCapabilityUnavailableError(
|
|
1355
|
+
"submitAssetWorkflow",
|
|
1356
|
+
skipped?.message
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Create a new source asset with both a record and bytes on disk.
|
|
1361
|
+
*
|
|
1362
|
+
* This is the same as `AssetStore.store()`, but exposed on the runtime
|
|
1363
|
+
* so callers only need one handle.
|
|
1364
|
+
*/
|
|
1365
|
+
storeSourceAsset(name, data, opts) {
|
|
1366
|
+
return this.store.store(name, data, opts);
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Create a derivative of `source`, persist its bytes, and optionally
|
|
1370
|
+
* record a provenance association.
|
|
1371
|
+
*
|
|
1372
|
+
* The new asset's `sourceAssetId` always points at `source.id`. When
|
|
1373
|
+
* `linkAssociation` is true (the default), the runtime also writes
|
|
1374
|
+
* an `AssetAssociation` so queries by role (e.g. "all `document_image`
|
|
1375
|
+
* derivatives for this `source_document`") work without scanning
|
|
1376
|
+
* `source_asset_id` chains.
|
|
1377
|
+
*/
|
|
1378
|
+
async storeDerivedAsset(source, name, data, opts) {
|
|
1379
|
+
if (!source.id) {
|
|
1380
|
+
throw new Error(
|
|
1381
|
+
"storeDerivedAsset: source asset must be persisted (missing id)"
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
const {
|
|
1385
|
+
role: rawRole,
|
|
1386
|
+
linkAssociation = true,
|
|
1387
|
+
derivativeMetaType = "Asset",
|
|
1388
|
+
...storeOpts
|
|
1389
|
+
} = opts;
|
|
1390
|
+
const role = rawRole ?? ASSET_ROLES.DERIVATION_SOURCE;
|
|
1391
|
+
const derived = await this.store.store(name, data, {
|
|
1392
|
+
...storeOpts,
|
|
1393
|
+
sourceAssetId: source.id
|
|
1394
|
+
});
|
|
1395
|
+
if (linkAssociation && derived.id) {
|
|
1396
|
+
await this.associations.attach(
|
|
1397
|
+
derivativeMetaType,
|
|
1398
|
+
derived.id,
|
|
1399
|
+
source.id,
|
|
1400
|
+
{ role }
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
return derived;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Record a provenance association between an existing source asset
|
|
1407
|
+
* and an existing derivative asset without touching bytes.
|
|
1408
|
+
*/
|
|
1409
|
+
async linkDerivation(source, derivative, opts = {}) {
|
|
1410
|
+
if (!source.id || !derivative.id) {
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
"linkDerivation: both source and derivative must be persisted (missing id)"
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
const role = opts.role ?? ASSET_ROLES.DERIVATION_SOURCE;
|
|
1416
|
+
const derivativeMetaType = opts.derivativeMetaType ?? "Asset";
|
|
1417
|
+
return this.associations.attach(
|
|
1418
|
+
derivativeMetaType,
|
|
1419
|
+
derivative.id,
|
|
1420
|
+
source.id,
|
|
1421
|
+
{ role }
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Update the standard extraction-status metadata on an asset's
|
|
1426
|
+
* `description` JSON sidecar. This is a thin convenience over the
|
|
1427
|
+
* convention in `asset-conventions.ts` — callers that store
|
|
1428
|
+
* metadata elsewhere can ignore it.
|
|
1429
|
+
*
|
|
1430
|
+
* **How existing descriptions are handled**:
|
|
1431
|
+
* - Empty / unset → fresh JSON object.
|
|
1432
|
+
* - Valid JSON object → merged into; existing keys preserved.
|
|
1433
|
+
* - Free-form prose or non-object JSON → preserved under the
|
|
1434
|
+
* reserved `text` key of the resulting object (e.g.
|
|
1435
|
+
* `{ text: "original prose", extractionStatus: "..." }`). No prose
|
|
1436
|
+
* is discarded.
|
|
1437
|
+
*
|
|
1438
|
+
* Callers that already use `text` for something else, or that need
|
|
1439
|
+
* an entirely separate metadata surface, should either round-trip
|
|
1440
|
+
* the JSON themselves or skip this helper — its only job is the
|
|
1441
|
+
* `extractionStatus` / `extractionError` / `extractedAt` triple.
|
|
1442
|
+
*
|
|
1443
|
+
* Error handling: when `status` transitions away from `failed`
|
|
1444
|
+
* without a new `extra.error`, the stale `extractionError` is
|
|
1445
|
+
* cleared so downstream consumers don't misread the current state.
|
|
1446
|
+
* When `status === 'succeeded'`, `extractedAt` is stamped to now
|
|
1447
|
+
* unless the caller provides one.
|
|
1448
|
+
*/
|
|
1449
|
+
async setExtractionStatus(asset, status, extra = {}) {
|
|
1450
|
+
const existing = parseDescriptionSidecar(asset.description);
|
|
1451
|
+
const next = {
|
|
1452
|
+
...existing,
|
|
1453
|
+
extractionStatus: status
|
|
1454
|
+
};
|
|
1455
|
+
if (extra.error !== void 0) {
|
|
1456
|
+
next.extractionError = extra.error;
|
|
1457
|
+
} else if (status !== "failed") {
|
|
1458
|
+
delete next.extractionError;
|
|
1459
|
+
}
|
|
1460
|
+
if (status === "succeeded") {
|
|
1461
|
+
next.extractedAt = (extra.extractedAt ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1462
|
+
}
|
|
1463
|
+
asset.description = JSON.stringify(next);
|
|
1464
|
+
await asset.save();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
async function createAssetRuntime(options) {
|
|
1468
|
+
const collection = options.collection ?? await AssetCollection.create({ db: options.db });
|
|
1469
|
+
const associations = options.associations ?? await AssetAssociationCollection.create({ db: options.db });
|
|
1470
|
+
const store = await new AssetStore(
|
|
1471
|
+
options.storage,
|
|
1472
|
+
collection,
|
|
1473
|
+
options.storeOptions
|
|
1474
|
+
).initialize();
|
|
1475
|
+
return new AssetRuntime(
|
|
1476
|
+
collection,
|
|
1477
|
+
associations,
|
|
1478
|
+
store,
|
|
1479
|
+
options.capabilityProviders
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
function parseDescriptionSidecar(description) {
|
|
1483
|
+
if (!description) return {};
|
|
1484
|
+
try {
|
|
1485
|
+
const parsed = JSON.parse(description);
|
|
1486
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1487
|
+
return parsed;
|
|
1488
|
+
}
|
|
1489
|
+
return { text: description };
|
|
1490
|
+
} catch {
|
|
1491
|
+
return { text: description };
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
const DEFAULT_REMOTE_MAX_BYTES = 50 * 1024 * 1024;
|
|
1495
|
+
const DEFAULT_REMOTE_TIMEOUT_MS = 1e4;
|
|
1496
|
+
const MAX_REMOTE_REDIRECTS = 5;
|
|
1497
|
+
const INLINE_SAFE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
1498
|
+
"application/json",
|
|
1499
|
+
"application/pdf",
|
|
1500
|
+
"audio/aac",
|
|
1501
|
+
"audio/mpeg",
|
|
1502
|
+
"audio/ogg",
|
|
1503
|
+
"audio/wav",
|
|
1504
|
+
"audio/webm",
|
|
1505
|
+
"image/avif",
|
|
1506
|
+
"image/gif",
|
|
1507
|
+
"image/jpeg",
|
|
1508
|
+
"image/png",
|
|
1509
|
+
"image/webp",
|
|
1510
|
+
"text/csv",
|
|
1511
|
+
"text/plain",
|
|
1512
|
+
"video/mp4",
|
|
1513
|
+
"video/ogg",
|
|
1514
|
+
"video/webm"
|
|
1515
|
+
]);
|
|
1516
|
+
function normalizeMimeType(raw) {
|
|
1517
|
+
return (raw.split(";")[0] ?? "").trim().toLowerCase();
|
|
1518
|
+
}
|
|
1519
|
+
function isInlineSafeMimeType(contentType) {
|
|
1520
|
+
return INLINE_SAFE_MIME_TYPES.has(normalizeMimeType(contentType));
|
|
1521
|
+
}
|
|
1522
|
+
function isRemoteUri(uri) {
|
|
1523
|
+
return /^https?:\/\//i.test(uri);
|
|
1524
|
+
}
|
|
1525
|
+
function isBlockedHost(hostname) {
|
|
1526
|
+
let host = hostname.trim().toLowerCase();
|
|
1527
|
+
if (host.startsWith("[") && host.endsWith("]")) {
|
|
1528
|
+
host = host.slice(1, -1);
|
|
1529
|
+
}
|
|
1530
|
+
host = host.replace(/\.+$/, "");
|
|
1531
|
+
if (host === "" || host === "localhost" || host.endsWith(".localhost")) {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
if (host === "169.254.169.254" || host === "metadata.google.internal" || host === "metadata" || host === "100.100.100.200") {
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1538
|
+
if (ipv4) {
|
|
1539
|
+
const octets = ipv4.slice(1).map((o) => Number(o));
|
|
1540
|
+
if (octets.some((o) => o > 255)) return true;
|
|
1541
|
+
return isBlockedIpv4(octets);
|
|
1542
|
+
}
|
|
1543
|
+
if (host.includes(":")) {
|
|
1544
|
+
return isBlockedIpv6(host);
|
|
1545
|
+
}
|
|
1546
|
+
return false;
|
|
1547
|
+
}
|
|
1548
|
+
function isBlockedIpv4(octets) {
|
|
1549
|
+
const [a, b] = octets;
|
|
1550
|
+
if (a === 0) return true;
|
|
1551
|
+
if (a === 10) return true;
|
|
1552
|
+
if (a === 127) return true;
|
|
1553
|
+
if (a === 169 && b === 254) return true;
|
|
1554
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
1555
|
+
if (a === 192 && b === 168) return true;
|
|
1556
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
1557
|
+
if (a >= 224) return true;
|
|
1558
|
+
return false;
|
|
1559
|
+
}
|
|
1560
|
+
function expandIpv6(host) {
|
|
1561
|
+
let h = host.toLowerCase();
|
|
1562
|
+
const pct = h.indexOf("%");
|
|
1563
|
+
if (pct !== -1) h = h.slice(0, pct);
|
|
1564
|
+
let tailHextets = [];
|
|
1565
|
+
const lastColon = h.lastIndexOf(":");
|
|
1566
|
+
const tail = lastColon === -1 ? "" : h.slice(lastColon + 1);
|
|
1567
|
+
if (tail.includes(".")) {
|
|
1568
|
+
const m = tail.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
|
1569
|
+
if (!m) return null;
|
|
1570
|
+
const octets = m.slice(1).map((o) => Number(o));
|
|
1571
|
+
if (octets.some((o) => o > 255)) return null;
|
|
1572
|
+
tailHextets = [octets[0] << 8 | octets[1], octets[2] << 8 | octets[3]];
|
|
1573
|
+
h = h.slice(0, lastColon);
|
|
1574
|
+
if (h === ":") h = "::";
|
|
1575
|
+
}
|
|
1576
|
+
const parseHextets = (parts) => {
|
|
1577
|
+
const out = [];
|
|
1578
|
+
for (const p of parts) {
|
|
1579
|
+
if (p === "" || !/^[0-9a-f]{1,4}$/.test(p)) return null;
|
|
1580
|
+
out.push(Number.parseInt(p, 16));
|
|
1581
|
+
}
|
|
1582
|
+
return out;
|
|
1583
|
+
};
|
|
1584
|
+
const doubleColon = h.indexOf("::");
|
|
1585
|
+
let head;
|
|
1586
|
+
let midTail;
|
|
1587
|
+
if (doubleColon !== -1) {
|
|
1588
|
+
if (h.indexOf("::", doubleColon + 1) !== -1) return null;
|
|
1589
|
+
const before = h.slice(0, doubleColon);
|
|
1590
|
+
const after = h.slice(doubleColon + 2);
|
|
1591
|
+
head = parseHextets(before === "" ? [] : before.split(":"));
|
|
1592
|
+
midTail = parseHextets(after === "" ? [] : after.split(":"));
|
|
1593
|
+
} else {
|
|
1594
|
+
head = parseHextets(h === "" ? [] : h.split(":"));
|
|
1595
|
+
midTail = [];
|
|
1596
|
+
}
|
|
1597
|
+
if (head === null || midTail === null) return null;
|
|
1598
|
+
const explicit = [...head, ...midTail, ...tailHextets];
|
|
1599
|
+
if (doubleColon !== -1) {
|
|
1600
|
+
if (explicit.length > 7) return null;
|
|
1601
|
+
const fill = 8 - explicit.length;
|
|
1602
|
+
return [...head, ...new Array(fill).fill(0), ...midTail, ...tailHextets];
|
|
1603
|
+
}
|
|
1604
|
+
if (explicit.length !== 8) return null;
|
|
1605
|
+
return explicit;
|
|
1606
|
+
}
|
|
1607
|
+
function isBlockedIpv6(host) {
|
|
1608
|
+
const hextets = expandIpv6(host);
|
|
1609
|
+
if (hextets === null) return true;
|
|
1610
|
+
const [h0, h1, h2, h3, h4, h5, h6, h7] = hextets;
|
|
1611
|
+
const embeddedV4 = () => [
|
|
1612
|
+
h6 >> 8 & 255,
|
|
1613
|
+
h6 & 255,
|
|
1614
|
+
h7 >> 8 & 255,
|
|
1615
|
+
h7 & 255
|
|
1616
|
+
];
|
|
1617
|
+
if (h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0 && h5 === 0) {
|
|
1618
|
+
if (h6 === 0 && h7 === 0) return true;
|
|
1619
|
+
if (h6 === 0 && h7 === 1) return true;
|
|
1620
|
+
return isBlockedIpv4(embeddedV4());
|
|
1621
|
+
}
|
|
1622
|
+
if (h0 === 0 && h1 === 0 && h2 === 0 && h3 === 0 && h4 === 0 && h5 === 65535) {
|
|
1623
|
+
return isBlockedIpv4(embeddedV4());
|
|
1624
|
+
}
|
|
1625
|
+
if ((h0 & 65472) === 65152) return true;
|
|
1626
|
+
if ((h0 & 65472) === 65216) return true;
|
|
1627
|
+
if ((h0 & 65024) === 64512) return true;
|
|
1628
|
+
if ((h0 & 65280) === 65280) return true;
|
|
1629
|
+
return false;
|
|
1630
|
+
}
|
|
1631
|
+
function assertSafeRemoteUrl(rawUrl) {
|
|
1632
|
+
let url;
|
|
1633
|
+
try {
|
|
1634
|
+
url = new URL(rawUrl);
|
|
1635
|
+
} catch {
|
|
1636
|
+
throw new AssetServeError("Invalid remote asset URL", 502);
|
|
1637
|
+
}
|
|
1638
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1639
|
+
throw new AssetServeError("Unsupported remote asset scheme", 502);
|
|
1640
|
+
}
|
|
1641
|
+
if (isBlockedHost(url.hostname)) {
|
|
1642
|
+
throw new AssetServeError("Remote asset host not allowed", 502);
|
|
1643
|
+
}
|
|
1644
|
+
return url;
|
|
1645
|
+
}
|
|
1646
|
+
async function fetchRemoteAsset(startUrl, fetchImpl, maxBytes, timeoutMs) {
|
|
1647
|
+
let current = assertSafeRemoteUrl(startUrl);
|
|
1648
|
+
for (let hop = 0; hop <= MAX_REMOTE_REDIRECTS; hop++) {
|
|
1649
|
+
const controller = new AbortController();
|
|
1650
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1651
|
+
let response;
|
|
1652
|
+
try {
|
|
1653
|
+
response = await fetchImpl(current.toString(), {
|
|
1654
|
+
redirect: "manual",
|
|
1655
|
+
signal: controller.signal
|
|
1656
|
+
});
|
|
1657
|
+
} finally {
|
|
1658
|
+
clearTimeout(timer);
|
|
1659
|
+
}
|
|
1660
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1661
|
+
const location = response.headers.get("location");
|
|
1662
|
+
if (!location) {
|
|
1663
|
+
throw new AssetServeError(
|
|
1664
|
+
"Remote asset redirect missing Location",
|
|
1665
|
+
502
|
|
1666
|
+
);
|
|
1667
|
+
}
|
|
1668
|
+
current = assertSafeRemoteUrl(new URL(location, current).toString());
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
if (!response.ok) {
|
|
1672
|
+
throw new AssetServeError(
|
|
1673
|
+
`Remote asset upstream returned ${response.status}`,
|
|
1674
|
+
502
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
const data = await readBodyWithCap(response, maxBytes);
|
|
1678
|
+
return {
|
|
1679
|
+
data,
|
|
1680
|
+
contentType: response.headers.get("content-type") ?? void 0
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
throw new AssetServeError("Too many remote asset redirects", 502);
|
|
1684
|
+
}
|
|
1685
|
+
async function readBodyWithCap(response, maxBytes) {
|
|
1686
|
+
const declared = response.headers.get("content-length");
|
|
1687
|
+
if (declared && Number(declared) > maxBytes) {
|
|
1688
|
+
throw new AssetServeError("Remote asset exceeds size limit", 502);
|
|
1689
|
+
}
|
|
1690
|
+
const body = response.body;
|
|
1691
|
+
if (body && typeof body.getReader === "function") {
|
|
1692
|
+
const reader = body.getReader();
|
|
1693
|
+
const chunks = [];
|
|
1694
|
+
let total = 0;
|
|
1695
|
+
while (true) {
|
|
1696
|
+
const { done, value } = await reader.read();
|
|
1697
|
+
if (done) break;
|
|
1698
|
+
if (value) {
|
|
1699
|
+
total += value.byteLength;
|
|
1700
|
+
if (total > maxBytes) {
|
|
1701
|
+
await reader.cancel().catch(() => {
|
|
1702
|
+
});
|
|
1703
|
+
throw new AssetServeError("Remote asset exceeds size limit", 502);
|
|
1704
|
+
}
|
|
1705
|
+
chunks.push(value);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
1709
|
+
}
|
|
1710
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
1711
|
+
if (buf.byteLength > maxBytes) {
|
|
1712
|
+
throw new AssetServeError("Remote asset exceeds size limit", 502);
|
|
1713
|
+
}
|
|
1714
|
+
return buf;
|
|
1715
|
+
}
|
|
1716
|
+
async function resolveAssetForServing(options) {
|
|
1717
|
+
const {
|
|
1718
|
+
runtime,
|
|
1719
|
+
asset: assetOrId,
|
|
1720
|
+
tenantId: tenantId2,
|
|
1721
|
+
canAccess,
|
|
1722
|
+
disposition: _disposition
|
|
1723
|
+
} = options;
|
|
1724
|
+
const asset = typeof assetOrId === "string" ? await runtime.collection.get({ id: assetOrId }) : assetOrId;
|
|
1725
|
+
if (!asset) {
|
|
1726
|
+
throw new AssetServeError("Asset not found", 404);
|
|
1727
|
+
}
|
|
1728
|
+
if (tenantId2 !== void 0 && asset.tenantId !== null && asset.tenantId !== tenantId2) {
|
|
1729
|
+
throw new AssetServeError("Asset not visible to this tenant", 403);
|
|
1730
|
+
}
|
|
1731
|
+
if (canAccess) {
|
|
1732
|
+
const allowed = await canAccess(asset);
|
|
1733
|
+
if (!allowed) {
|
|
1734
|
+
throw new AssetServeError("Asset access denied", 403);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const remoteMode = options.remoteMode ?? "error";
|
|
1738
|
+
let data;
|
|
1739
|
+
let remoteContentType;
|
|
1740
|
+
if (isRemoteUri(asset.sourceUri)) {
|
|
1741
|
+
if (remoteMode === "error") {
|
|
1742
|
+
throw new AssetServeError("Remote asset not supported", 500);
|
|
1743
|
+
}
|
|
1744
|
+
if (remoteMode === "redirect") {
|
|
1745
|
+
throw new RedirectToSourceUri(asset.sourceUri);
|
|
1746
|
+
}
|
|
1747
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1748
|
+
if (!fetchImpl) {
|
|
1749
|
+
throw new AssetServeError(
|
|
1750
|
+
"No fetch implementation available for remote asset",
|
|
1751
|
+
500
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
const maxBytes = options.remoteMaxBytes ?? DEFAULT_REMOTE_MAX_BYTES;
|
|
1755
|
+
const timeoutMs = options.remoteTimeoutMs ?? DEFAULT_REMOTE_TIMEOUT_MS;
|
|
1756
|
+
try {
|
|
1757
|
+
const result = await fetchRemoteAsset(
|
|
1758
|
+
asset.sourceUri,
|
|
1759
|
+
fetchImpl,
|
|
1760
|
+
maxBytes,
|
|
1761
|
+
timeoutMs
|
|
1762
|
+
);
|
|
1763
|
+
data = result.data;
|
|
1764
|
+
remoteContentType = result.contentType;
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
if (err instanceof AssetServeError) throw err;
|
|
1767
|
+
console.error("serveAsset: remote fetch failed", err);
|
|
1768
|
+
throw new AssetServeError("Failed to fetch remote asset", 502);
|
|
1769
|
+
}
|
|
1770
|
+
} else {
|
|
1771
|
+
try {
|
|
1772
|
+
data = await runtime.store.read(asset);
|
|
1773
|
+
} catch (err) {
|
|
1774
|
+
console.error("serveAsset: store read failed", err);
|
|
1775
|
+
throw new AssetServeError(
|
|
1776
|
+
"Failed to read asset bytes",
|
|
1777
|
+
500,
|
|
1778
|
+
"Failed to read asset bytes"
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const contentType = asset.mimeType || remoteContentType || "application/octet-stream";
|
|
1783
|
+
const filename = options.filename ?? deriveFilename(asset);
|
|
1784
|
+
return {
|
|
1785
|
+
asset,
|
|
1786
|
+
data,
|
|
1787
|
+
contentType,
|
|
1788
|
+
filename,
|
|
1789
|
+
size: data.byteLength
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1792
|
+
class RedirectToSourceUri extends Error {
|
|
1793
|
+
constructor(location) {
|
|
1794
|
+
super("Asset stored at remote URL");
|
|
1795
|
+
this.location = location;
|
|
1796
|
+
this.name = "RedirectToSourceUri";
|
|
1797
|
+
}
|
|
1798
|
+
location;
|
|
1799
|
+
}
|
|
1800
|
+
async function serveAsset(options) {
|
|
1801
|
+
const Ctor = options.responseCtor ?? globalThis.Response;
|
|
1802
|
+
if (!Ctor) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
"serveAsset: no Response constructor available. Pass options.responseCtor or run on Node 18+."
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
try {
|
|
1808
|
+
const { data, contentType, filename, size } = await resolveAssetForServing(options);
|
|
1809
|
+
const requestedDisposition = options.disposition ?? "inline";
|
|
1810
|
+
const disposition = requestedDisposition === "attachment" || isInlineSafeMimeType(contentType) ? requestedDisposition : "attachment";
|
|
1811
|
+
const PROTECTED_HEADERS = /* @__PURE__ */ new Set([
|
|
1812
|
+
"content-type",
|
|
1813
|
+
"content-length",
|
|
1814
|
+
"content-disposition",
|
|
1815
|
+
"x-content-type-options"
|
|
1816
|
+
]);
|
|
1817
|
+
const headers = {};
|
|
1818
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
1819
|
+
if (!PROTECTED_HEADERS.has(name.toLowerCase())) {
|
|
1820
|
+
headers[name] = value;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
headers["Content-Type"] = contentType;
|
|
1824
|
+
headers["Content-Length"] = String(size);
|
|
1825
|
+
headers["Content-Disposition"] = buildContentDisposition(
|
|
1826
|
+
disposition,
|
|
1827
|
+
filename
|
|
1828
|
+
);
|
|
1829
|
+
headers["X-Content-Type-Options"] = "nosniff";
|
|
1830
|
+
return new Ctor(
|
|
1831
|
+
data,
|
|
1832
|
+
{
|
|
1833
|
+
status: 200,
|
|
1834
|
+
headers
|
|
1835
|
+
}
|
|
1836
|
+
);
|
|
1837
|
+
} catch (err) {
|
|
1838
|
+
if (err instanceof RedirectToSourceUri) {
|
|
1839
|
+
return new Ctor(null, {
|
|
1840
|
+
status: 302,
|
|
1841
|
+
headers: { Location: err.location }
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
if (err instanceof AssetServeError) {
|
|
1845
|
+
if (err.status >= 500) {
|
|
1846
|
+
console.error("serveAsset: request failed", err.status, err.message);
|
|
1847
|
+
}
|
|
1848
|
+
return new Ctor(err.clientMessage, {
|
|
1849
|
+
status: err.status,
|
|
1850
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
console.error("serveAsset: unexpected error", err);
|
|
1854
|
+
return new Ctor("Internal error serving asset", {
|
|
1855
|
+
status: 500,
|
|
1856
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" }
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function defaultClientMessage(status) {
|
|
1861
|
+
switch (status) {
|
|
1862
|
+
case 403:
|
|
1863
|
+
return "Forbidden";
|
|
1864
|
+
case 404:
|
|
1865
|
+
return "Not found";
|
|
1866
|
+
case 502:
|
|
1867
|
+
return "Bad gateway";
|
|
1868
|
+
default:
|
|
1869
|
+
return "Internal error serving asset";
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
class AssetServeError extends Error {
|
|
1873
|
+
constructor(message, status, clientMessage) {
|
|
1874
|
+
super(message);
|
|
1875
|
+
this.status = status;
|
|
1876
|
+
this.name = "AssetServeError";
|
|
1877
|
+
this.clientMessage = clientMessage ?? defaultClientMessage(status);
|
|
1878
|
+
}
|
|
1879
|
+
status;
|
|
1880
|
+
/** Opaque body safe to return to the HTTP client. */
|
|
1881
|
+
clientMessage;
|
|
1882
|
+
}
|
|
1883
|
+
function deriveFilename(asset) {
|
|
1884
|
+
if (asset.name) return asset.name;
|
|
1885
|
+
const fromUri = extractBasename(asset.sourceUri);
|
|
1886
|
+
if (fromUri) return fromUri;
|
|
1887
|
+
return asset.id ?? "asset";
|
|
1888
|
+
}
|
|
1889
|
+
function extractBasename(uri) {
|
|
1890
|
+
if (!uri) return "";
|
|
1891
|
+
const noScheme = uri.replace(/^[a-z][a-z0-9+\-.]*:\/\//i, "");
|
|
1892
|
+
const last = noScheme.split("/").pop() ?? "";
|
|
1893
|
+
return last;
|
|
1894
|
+
}
|
|
1895
|
+
function sanitizeDispositionFilename(filename) {
|
|
1896
|
+
let stripped = "";
|
|
1897
|
+
for (const ch of filename) {
|
|
1898
|
+
const code = ch.charCodeAt(0);
|
|
1899
|
+
if (code > 31 && code !== 127) {
|
|
1900
|
+
stripped += ch;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
const safe = stripped.replace(/["\\/]/g, "_").trim();
|
|
1904
|
+
return safe || "asset";
|
|
1905
|
+
}
|
|
1906
|
+
function buildContentDisposition(disposition, filename) {
|
|
1907
|
+
const sanitized = sanitizeDispositionFilename(filename);
|
|
1908
|
+
const encoded = encodeURIComponent(sanitized);
|
|
1909
|
+
return `${disposition}; filename="${sanitized}"; filename*=UTF-8''${encoded}`;
|
|
1910
|
+
}
|
|
1911
|
+
class AssetStatusCollection extends SmrtCollection {
|
|
1912
|
+
static _itemClass = AssetStatus;
|
|
1913
|
+
/**
|
|
1914
|
+
* Get or create an asset status by slug
|
|
1915
|
+
*
|
|
1916
|
+
* @param slug - The asset status slug
|
|
1917
|
+
* @param name - The display name (defaults to slug)
|
|
1918
|
+
* @param description - Optional description
|
|
1919
|
+
* @returns The existing or newly created AssetStatus
|
|
1920
|
+
*/
|
|
1921
|
+
async getOrCreate(slug, name, description) {
|
|
1922
|
+
const existing = await this.list({ where: { slug }, limit: 1 });
|
|
1923
|
+
if (existing.length > 0) {
|
|
1924
|
+
return existing[0];
|
|
1925
|
+
}
|
|
1926
|
+
return await this.create({
|
|
1927
|
+
slug,
|
|
1928
|
+
name: name || slug,
|
|
1929
|
+
description
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
/**
|
|
1933
|
+
* Initialize common asset statuses
|
|
1934
|
+
*
|
|
1935
|
+
* Creates standard asset statuses if they don't exist:
|
|
1936
|
+
* - draft
|
|
1937
|
+
* - published
|
|
1938
|
+
* - archived
|
|
1939
|
+
* - deleted
|
|
1940
|
+
*/
|
|
1941
|
+
async initializeCommonStatuses() {
|
|
1942
|
+
await this.getOrCreate("draft", "Draft", "Work in progress, not yet ready");
|
|
1943
|
+
await this.getOrCreate("published", "Published", "Live and available");
|
|
1944
|
+
await this.getOrCreate(
|
|
1945
|
+
"archived",
|
|
1946
|
+
"Archived",
|
|
1947
|
+
"No longer active but preserved"
|
|
1948
|
+
);
|
|
1949
|
+
await this.getOrCreate("deleted", "Deleted", "Marked for deletion");
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
class AssetTypeCollection extends SmrtCollection {
|
|
1953
|
+
static _itemClass = AssetType;
|
|
1954
|
+
/**
|
|
1955
|
+
* Get or create an asset type by slug
|
|
1956
|
+
*
|
|
1957
|
+
* @param slug - The asset type slug
|
|
1958
|
+
* @param name - The display name (defaults to slug)
|
|
1959
|
+
* @param description - Optional description
|
|
1960
|
+
* @returns The existing or newly created AssetType
|
|
1961
|
+
*/
|
|
1962
|
+
async getOrCreate(slug, name, description) {
|
|
1963
|
+
const existing = await this.list({ where: { slug }, limit: 1 });
|
|
1964
|
+
if (existing.length > 0) {
|
|
1965
|
+
return existing[0];
|
|
1966
|
+
}
|
|
1967
|
+
return await this.create({
|
|
1968
|
+
slug,
|
|
1969
|
+
name: name || slug,
|
|
1970
|
+
description
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Initialize common asset types
|
|
1975
|
+
*
|
|
1976
|
+
* Creates standard asset types if they don't exist:
|
|
1977
|
+
* - image
|
|
1978
|
+
* - video
|
|
1979
|
+
* - document
|
|
1980
|
+
* - audio
|
|
1981
|
+
*
|
|
1982
|
+
* Note: `folder` was removed in R3-D. Folders are no longer an asset
|
|
1983
|
+
* subtype — they live on their own `folders` table. See `folder.ts`.
|
|
1984
|
+
*/
|
|
1985
|
+
async initializeCommonTypes() {
|
|
1986
|
+
await this.getOrCreate(
|
|
1987
|
+
"image",
|
|
1988
|
+
"Image",
|
|
1989
|
+
"Image files (JPEG, PNG, GIF, etc.)"
|
|
1990
|
+
);
|
|
1991
|
+
await this.getOrCreate(
|
|
1992
|
+
"video",
|
|
1993
|
+
"Video",
|
|
1994
|
+
"Video files (MP4, AVI, MOV, etc.)"
|
|
1995
|
+
);
|
|
1996
|
+
await this.getOrCreate(
|
|
1997
|
+
"document",
|
|
1998
|
+
"Document",
|
|
1999
|
+
"Document files (PDF, DOCX, TXT, etc.)"
|
|
2000
|
+
);
|
|
2001
|
+
await this.getOrCreate(
|
|
2002
|
+
"audio",
|
|
2003
|
+
"Audio",
|
|
2004
|
+
"Audio files (MP3, WAV, AAC, etc.)"
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
var __defProp = Object.defineProperty;
|
|
2009
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
2010
|
+
var __decorateClass = (decorators, target, key, kind) => {
|
|
2011
|
+
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
2012
|
+
for (var i = decorators.length - 1, decorator; i >= 0; i--)
|
|
2013
|
+
if (decorator = decorators[i])
|
|
2014
|
+
result = (kind ? decorator(target, key, result) : decorator(result)) || result;
|
|
2015
|
+
if (kind && result) __defProp(target, key, result);
|
|
2016
|
+
return result;
|
|
2017
|
+
};
|
|
2018
|
+
let Folder = class extends SmrtHierarchical {
|
|
2019
|
+
tenantId = null;
|
|
2020
|
+
// Core fields. `parentId` is inherited from SmrtHierarchical.
|
|
2021
|
+
name = "";
|
|
2022
|
+
// `slug` is inherited as an accessor from SmrtObject.
|
|
2023
|
+
description = "";
|
|
2024
|
+
ownerProfileId = null;
|
|
2025
|
+
// Timestamps
|
|
2026
|
+
createdAt = /* @__PURE__ */ new Date();
|
|
2027
|
+
updatedAt = /* @__PURE__ */ new Date();
|
|
2028
|
+
constructor(options = {}) {
|
|
2029
|
+
super(options);
|
|
2030
|
+
if (options.name !== void 0) this.name = options.name;
|
|
2031
|
+
if (options.slug !== void 0) this.slug = options.slug;
|
|
2032
|
+
if (options.description !== void 0)
|
|
2033
|
+
this.description = options.description;
|
|
2034
|
+
if (options.parentId !== void 0)
|
|
2035
|
+
this.parentId = options.parentId ?? null;
|
|
2036
|
+
if (options.ownerProfileId !== void 0)
|
|
2037
|
+
this.ownerProfileId = options.ownerProfileId;
|
|
2038
|
+
if (options.tenantId !== void 0) this.tenantId = options.tenantId;
|
|
2039
|
+
if (options.createdAt) this.createdAt = options.createdAt;
|
|
2040
|
+
if (options.updatedAt) this.updatedAt = options.updatedAt;
|
|
2041
|
+
}
|
|
2042
|
+
// Hierarchy traversal (getParent / getChildren / getAncestors /
|
|
2043
|
+
// getDescendants / getHierarchy / moveTo) provided by SmrtHierarchical.
|
|
2044
|
+
/**
|
|
2045
|
+
* Look up a folder by slug.
|
|
2046
|
+
*/
|
|
2047
|
+
static async getBySlug(_slug) {
|
|
2048
|
+
return null;
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
__decorateClass([
|
|
2052
|
+
tenantId({ nullable: true })
|
|
2053
|
+
], Folder.prototype, "tenantId", 2);
|
|
2054
|
+
__decorateClass([
|
|
2055
|
+
crossPackageRef("@happyvertical/smrt-profiles:Profile")
|
|
2056
|
+
], Folder.prototype, "ownerProfileId", 2);
|
|
2057
|
+
Folder = __decorateClass([
|
|
2058
|
+
TenantScoped({ mode: "optional" }),
|
|
2059
|
+
smrt({
|
|
2060
|
+
api: { include: ["list", "get", "create", "update", "delete"] },
|
|
2061
|
+
mcp: { include: ["list", "get", "create", "update"] },
|
|
2062
|
+
cli: true
|
|
2063
|
+
})
|
|
2064
|
+
], Folder);
|
|
2065
|
+
class FolderCollection extends SmrtCollection {
|
|
2066
|
+
static _itemClass = Folder;
|
|
2067
|
+
/**
|
|
2068
|
+
* Get the folder tree starting from an optional root.
|
|
2069
|
+
*
|
|
2070
|
+
* When `rootId` is omitted, returns top-level folders (those with no
|
|
2071
|
+
* parent). When `rootId` is provided, returns all descendants of that
|
|
2072
|
+
* folder via the inherited SmrtHierarchical BFS traversal — same
|
|
2073
|
+
* cycle-safe, one-query-per-depth behaviour as Place/Event.
|
|
2074
|
+
*
|
|
2075
|
+
* @param rootId - Optional root folder ID; if omitted, returns top-level folders
|
|
2076
|
+
* @returns Array of folders (flat list; use parentId to reconstruct tree)
|
|
2077
|
+
*/
|
|
2078
|
+
async getTree(rootId) {
|
|
2079
|
+
if (!rootId) {
|
|
2080
|
+
return await this.list({
|
|
2081
|
+
where: { parentId: null },
|
|
2082
|
+
orderBy: "name ASC"
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
const result = [];
|
|
2086
|
+
const queue = [rootId];
|
|
2087
|
+
const visited = /* @__PURE__ */ new Set([rootId]);
|
|
2088
|
+
while (queue.length > 0) {
|
|
2089
|
+
const currentId = queue.shift();
|
|
2090
|
+
const children = await this.list({
|
|
2091
|
+
where: { parentId: currentId },
|
|
2092
|
+
orderBy: "name ASC"
|
|
2093
|
+
});
|
|
2094
|
+
for (const child of children) {
|
|
2095
|
+
if (child.id && !visited.has(child.id)) {
|
|
2096
|
+
visited.add(child.id);
|
|
2097
|
+
result.push(child);
|
|
2098
|
+
queue.push(child.id);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
return result;
|
|
2103
|
+
}
|
|
2104
|
+
/**
|
|
2105
|
+
* Get the path from root to a given folder (ancestors + self).
|
|
2106
|
+
*
|
|
2107
|
+
* @param folderId - The folder ID to get the path for
|
|
2108
|
+
* @returns Array of folders from root to the given folder (inclusive)
|
|
2109
|
+
*/
|
|
2110
|
+
async getPath(folderId) {
|
|
2111
|
+
const folder = await this.get({ id: folderId });
|
|
2112
|
+
if (!folder) return [];
|
|
2113
|
+
const ancestors = await folder.getAncestors();
|
|
2114
|
+
return [...ancestors, folder];
|
|
2115
|
+
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Get all assets that are direct children of a folder.
|
|
2118
|
+
*
|
|
2119
|
+
* Folder membership is recorded on `Asset.folderId`, not on the folder
|
|
2120
|
+
* itself, so this is delegated to the asset collection.
|
|
2121
|
+
*
|
|
2122
|
+
* @param folderId - The folder ID
|
|
2123
|
+
* @param assetCollection - An AssetCollection instance for querying
|
|
2124
|
+
* @returns Array of assets in this folder
|
|
2125
|
+
*/
|
|
2126
|
+
async getContents(folderId, assetCollection) {
|
|
2127
|
+
return await assetCollection.list({
|
|
2128
|
+
where: { folderId }
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Move an asset into a folder (or out, by passing `null`).
|
|
2133
|
+
*
|
|
2134
|
+
* @param asset - The asset to move
|
|
2135
|
+
* @param folderId - The target folder ID (or null to move to root)
|
|
2136
|
+
*/
|
|
2137
|
+
async moveAsset(asset, folderId) {
|
|
2138
|
+
asset.folderId = folderId;
|
|
2139
|
+
await asset.save();
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
async function persistMediaBundleInspection(adapter, inspection, options = {}) {
|
|
2143
|
+
const warnings = [...inspection.warnings];
|
|
2144
|
+
const primary = await adapter.upsertAsset({
|
|
2145
|
+
file: inspection.primary,
|
|
2146
|
+
role: "primary",
|
|
2147
|
+
typeSlug: options.primaryTypeSlug,
|
|
2148
|
+
metadata: inspection.metadata,
|
|
2149
|
+
inspection
|
|
2150
|
+
});
|
|
2151
|
+
const supportAssetIds = [];
|
|
2152
|
+
for (const support of inspection.supportFiles) {
|
|
2153
|
+
const visibility = support.visibility ?? options.supportVisibility ?? "hidden-retained";
|
|
2154
|
+
if (visibility === "drop-after-extract") continue;
|
|
2155
|
+
const supportAsset = await adapter.upsertAsset({
|
|
2156
|
+
file: support.file,
|
|
2157
|
+
role: "support",
|
|
2158
|
+
typeSlug: options.supportTypeSlug,
|
|
2159
|
+
parentAssetId: primary.id,
|
|
2160
|
+
relationship: support.relationship,
|
|
2161
|
+
visibility,
|
|
2162
|
+
metadata: support.metadata ?? {},
|
|
2163
|
+
inspection
|
|
2164
|
+
});
|
|
2165
|
+
supportAssetIds.push(supportAsset.id);
|
|
2166
|
+
await adapter.associateSupportFile?.({
|
|
2167
|
+
primaryAssetId: primary.id,
|
|
2168
|
+
supportAssetId: supportAsset.id,
|
|
2169
|
+
relationship: support.relationship,
|
|
2170
|
+
visibility,
|
|
2171
|
+
metadata: support.metadata
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
let metadataArtifactId;
|
|
2175
|
+
if (options.writeMetadataArtifact !== false && adapter.writeMetadataArtifact) {
|
|
2176
|
+
const artifact = await adapter.writeMetadataArtifact({
|
|
2177
|
+
primaryAssetId: primary.id,
|
|
2178
|
+
inspection
|
|
2179
|
+
});
|
|
2180
|
+
metadataArtifactId = artifact?.id;
|
|
2181
|
+
}
|
|
2182
|
+
let gpsTrackPointCount = 0;
|
|
2183
|
+
const gpsTrack = inspection.metadata.gpsTrack ?? [];
|
|
2184
|
+
if (options.writeGpsTrack !== false && gpsTrack.length > 0) {
|
|
2185
|
+
if (adapter.replaceGpsTrack) {
|
|
2186
|
+
gpsTrackPointCount = await adapter.replaceGpsTrack(primary.id, gpsTrack);
|
|
2187
|
+
} else {
|
|
2188
|
+
warnings.push("adapter cannot persist gps track");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
primaryAssetId: primary.id,
|
|
2193
|
+
supportAssetIds,
|
|
2194
|
+
metadataArtifactId,
|
|
2195
|
+
gpsTrackPointCount,
|
|
2196
|
+
warnings
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const OWNED_ASSET_RELATIONSHIP_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
2200
|
+
function assertValidOwnedAssetRelationship(relationship) {
|
|
2201
|
+
if (!OWNED_ASSET_RELATIONSHIP_PATTERN.test(relationship)) {
|
|
2202
|
+
throw new Error(
|
|
2203
|
+
`Invalid relationship type "${relationship}"; must start with a letter or underscore and contain only letters, digits, and underscores`
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
function assertValidOwnedAssetSortOrder(sortOrder) {
|
|
2208
|
+
if (!Number.isInteger(sortOrder) || sortOrder < 0 || sortOrder > 2147483647) {
|
|
2209
|
+
throw new Error(
|
|
2210
|
+
`Invalid sortOrder "${sortOrder}"; must be a non-negative integer`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async function resolveOwnedAssetsById(db, assetIds, tenantId2) {
|
|
2215
|
+
if (assetIds.length === 0) {
|
|
2216
|
+
return [];
|
|
2217
|
+
}
|
|
2218
|
+
const assets = await AssetCollection.create({ db });
|
|
2219
|
+
const resolved = tenantId2 ? await withSystemContext(async () => assets.listByIds(assetIds)) : await assets.listByIds(assetIds);
|
|
2220
|
+
const visibleAssets = tenantId2 ? resolved.filter(
|
|
2221
|
+
(asset) => asset.tenantId === tenantId2 || asset.tenantId === null
|
|
2222
|
+
) : resolved;
|
|
2223
|
+
const assetsById = new Map(
|
|
2224
|
+
visibleAssets.filter((asset) => asset.id).map((asset) => [asset.id, asset])
|
|
2225
|
+
);
|
|
2226
|
+
return assetIds.map((assetId) => assetsById.get(assetId)).filter(Boolean);
|
|
2227
|
+
}
|
|
2228
|
+
async function getOwnerRecord(collection, ownerId) {
|
|
2229
|
+
return collection.get({ id: ownerId });
|
|
2230
|
+
}
|
|
2231
|
+
async function getOwnedAssetsFromCollection(collection, ownerId, relationship) {
|
|
2232
|
+
const owner = await getOwnerRecord(collection, ownerId);
|
|
2233
|
+
if (!owner) {
|
|
2234
|
+
return [];
|
|
2235
|
+
}
|
|
2236
|
+
return owner.getAssets(relationship);
|
|
2237
|
+
}
|
|
2238
|
+
async function addOwnedAssetFromCollection(collection, ownerType, ownerId, asset, relationship = "attachment", sortOrder = 0) {
|
|
2239
|
+
const owner = await getOwnerRecord(collection, ownerId);
|
|
2240
|
+
if (!owner) {
|
|
2241
|
+
throw new Error(`${ownerType} '${ownerId}' not found`);
|
|
2242
|
+
}
|
|
2243
|
+
await owner.addAsset(asset, relationship, sortOrder);
|
|
2244
|
+
}
|
|
2245
|
+
async function removeOwnedAssetFromCollection(collection, ownerType, ownerId, assetId, relationship) {
|
|
2246
|
+
const owner = await getOwnerRecord(collection, ownerId);
|
|
2247
|
+
if (!owner) {
|
|
2248
|
+
throw new Error(`${ownerType} '${ownerId}' not found`);
|
|
2249
|
+
}
|
|
2250
|
+
await owner.removeAsset(assetId, relationship);
|
|
2251
|
+
}
|
|
2252
|
+
export {
|
|
2253
|
+
ASSET_EXTRACTION_STATUS,
|
|
2254
|
+
ASSET_METADATA_KEYS,
|
|
2255
|
+
ASSET_ROLES,
|
|
2256
|
+
Asset,
|
|
2257
|
+
AssetAssociation,
|
|
2258
|
+
AssetAssociationCollection,
|
|
2259
|
+
AssetCapabilitySkippedError,
|
|
2260
|
+
AssetCapabilityUnavailableError,
|
|
2261
|
+
AssetCollection,
|
|
2262
|
+
AssetMetafield,
|
|
2263
|
+
AssetMetafieldCollection,
|
|
2264
|
+
AssetRuntime,
|
|
2265
|
+
AssetServeError,
|
|
2266
|
+
AssetStatus,
|
|
2267
|
+
AssetStatusCollection,
|
|
2268
|
+
AssetStore,
|
|
2269
|
+
AssetType,
|
|
2270
|
+
AssetTypeCollection,
|
|
2271
|
+
Folder,
|
|
2272
|
+
FolderCollection,
|
|
2273
|
+
OWNED_ASSET_RELATIONSHIP_PATTERN,
|
|
2274
|
+
addOwnedAssetFromCollection,
|
|
2275
|
+
assertValidOwnedAssetRelationship,
|
|
2276
|
+
assertValidOwnedAssetSortOrder,
|
|
2277
|
+
createAssetRuntime,
|
|
2278
|
+
getOwnedAssetsFromCollection,
|
|
2279
|
+
persistMediaBundleInspection,
|
|
2280
|
+
removeOwnedAssetFromCollection,
|
|
2281
|
+
resolveAssetForServing,
|
|
2282
|
+
resolveOwnedAssetsById,
|
|
2283
|
+
serveAsset
|
|
2284
|
+
};
|
|
2285
|
+
//# sourceMappingURL=index.js.map
|