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