@burojs/file-storage 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +808 -822
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -300
- package/dist/index.d.ts +321 -300
- package/dist/index.js +807 -820
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -1,880 +1,866 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var FILE_STORAGE_SERVICE = "fileStorage";
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/registry.ts
|
|
3
|
+
const FILE_STORAGE_SERVICE = "fileStorage";
|
|
5
4
|
function createFileStorageRegistry(map) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
has: (name) => entries.has(name),
|
|
23
|
-
names: () => [...entries.keys()],
|
|
24
|
-
hasFused: () => [...entries.values()].some((a) => a.pipeline === "fused")
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// src/plugin.ts
|
|
5
|
+
if (!map.default) throw new Error("fileStoragePlugin requires a 'default' entry — every file field that does not name a storage resolves to it.");
|
|
6
|
+
const entries = new Map(Object.entries(map));
|
|
7
|
+
return {
|
|
8
|
+
get(name = "default") {
|
|
9
|
+
const found = entries.get(name);
|
|
10
|
+
if (!found) throw new Error(`Unknown file storage '${name}'. Registered: ${[...entries.keys()].join(", ")}.`);
|
|
11
|
+
return found;
|
|
12
|
+
},
|
|
13
|
+
has: (name) => entries.has(name),
|
|
14
|
+
names: () => [...entries.keys()],
|
|
15
|
+
hasFused: () => [...entries.values()].some((a) => a.pipeline === "fused")
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/plugin.ts
|
|
29
20
|
function fileStoragePlugin(map) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
21
|
+
const adapters = Object.freeze({ ...map });
|
|
22
|
+
return {
|
|
23
|
+
name: "@burojs/file-storage",
|
|
24
|
+
enforce: "pre",
|
|
25
|
+
setup: (api) => {
|
|
26
|
+
api.runtime.registerService(FILE_STORAGE_SERVICE, () => createFileStorageRegistry(adapters));
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/mappers/ref.ts
|
|
32
|
+
let seq = 0;
|
|
42
33
|
function newRef(init = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
seq += 1;
|
|
35
|
+
return {
|
|
36
|
+
localId: `ref-${seq}`,
|
|
37
|
+
...init
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/kind.ts
|
|
48
42
|
function isImage(ref) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
if (ref.mime) return ref.mime.startsWith("image/");
|
|
44
|
+
const name = ref.name ?? "";
|
|
45
|
+
return /\.(png|jpe?g|gif|webp|svg|avif)$/i.test(name);
|
|
52
46
|
}
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/mappers/scalar-handle.ts
|
|
55
49
|
function scalarHandle() {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
50
|
+
return {
|
|
51
|
+
carriesPending: true,
|
|
52
|
+
read(column) {
|
|
53
|
+
if (column == null) return [];
|
|
54
|
+
return (Array.isArray(column) ? column : [column]).filter((entry) => entry !== "" && entry != null).map((entry) => newRef({
|
|
55
|
+
handle: entry,
|
|
56
|
+
raw: entry,
|
|
57
|
+
name: tail(entry)
|
|
58
|
+
}));
|
|
59
|
+
},
|
|
60
|
+
write(next, _prev, ctx) {
|
|
61
|
+
const values = next.map((r) => r.handle ?? r.pending).filter((v) => v != null);
|
|
62
|
+
if (ctx.multiple) return values;
|
|
63
|
+
return values[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
69
66
|
}
|
|
70
67
|
function tail(value) {
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
const seg = value.split("/").pop();
|
|
69
|
+
return seg && seg !== value ? seg : value || void 0;
|
|
73
70
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/mappers/rich-image.ts
|
|
73
|
+
const DEFAULT_USAGE_KEYS = ["hotspot", "crop"];
|
|
77
74
|
function richImage(opts) {
|
|
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
|
-
if (ctx.multiple) return out;
|
|
110
|
-
return out[0] ?? null;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
75
|
+
const handleKey = opts.handleKey ?? "id";
|
|
76
|
+
const usageKeys = opts.usageKeys ?? DEFAULT_USAGE_KEYS;
|
|
77
|
+
function readOne(entry) {
|
|
78
|
+
const usage = opts.focus === "usage" ? pick(entry, usageKeys.filter((k) => entry[k] !== void 0)) : void 0;
|
|
79
|
+
return newRef({
|
|
80
|
+
handle: asString$1(getPath(entry, handleKey)),
|
|
81
|
+
raw: entry,
|
|
82
|
+
name: asString$1(entry.name ?? entry.filename_download),
|
|
83
|
+
mime: asString$1(entry.mime ?? entry.type),
|
|
84
|
+
size: asNumber(entry.size ?? entry.filesize),
|
|
85
|
+
width: asNumber(entry.width),
|
|
86
|
+
height: asNumber(entry.height),
|
|
87
|
+
...usage && Object.keys(usage).length > 0 ? { usage } : {}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
read(column, ctx) {
|
|
92
|
+
if (column == null) return [];
|
|
93
|
+
return (Array.isArray(column) ? column : [column]).filter((e) => e && typeof e === "object").map(readOne);
|
|
94
|
+
},
|
|
95
|
+
write(next, _prev, ctx) {
|
|
96
|
+
const out = next.filter((r) => r.handle != null).map((r) => {
|
|
97
|
+
const base = isObj(r.raw) ? { ...r.raw } : {};
|
|
98
|
+
setPath(base, handleKey, r.handle);
|
|
99
|
+
if (opts.focus === "usage" && r.usage) Object.assign(base, r.usage);
|
|
100
|
+
return base;
|
|
101
|
+
});
|
|
102
|
+
if (ctx.multiple) return out;
|
|
103
|
+
return out[0] ?? null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
113
106
|
}
|
|
114
107
|
function isObj(v) {
|
|
115
|
-
|
|
108
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
116
109
|
}
|
|
117
|
-
function asString(v) {
|
|
118
|
-
|
|
110
|
+
function asString$1(v) {
|
|
111
|
+
return typeof v === "string" ? v : void 0;
|
|
119
112
|
}
|
|
120
113
|
function asNumber(v) {
|
|
121
|
-
|
|
114
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
122
115
|
}
|
|
123
116
|
function pick(src, keys) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
117
|
+
const out = {};
|
|
118
|
+
for (const k of keys) out[k] = src[k];
|
|
119
|
+
return out;
|
|
127
120
|
}
|
|
128
121
|
function getPath(src, path) {
|
|
129
|
-
|
|
122
|
+
return path.split(".").reduce((acc, k) => isObj(acc) ? acc[k] : void 0, src);
|
|
130
123
|
}
|
|
131
124
|
function setPath(target, path, value) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
|
|
125
|
+
const keys = path.split(".");
|
|
126
|
+
const last = keys.pop();
|
|
127
|
+
let cursor = target;
|
|
128
|
+
for (const k of keys) {
|
|
129
|
+
const nextLevel = cursor[k];
|
|
130
|
+
cursor[k] = isObj(nextLevel) ? { ...nextLevel } : {};
|
|
131
|
+
cursor = cursor[k];
|
|
132
|
+
}
|
|
133
|
+
cursor[last] = value;
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/mappers/junction.ts
|
|
144
137
|
function junction(opts) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
138
|
+
return {
|
|
139
|
+
read(column, _ctx) {
|
|
140
|
+
if (!Array.isArray(column)) return [];
|
|
141
|
+
return column.filter((row) => row && typeof row === "object").map((row) => newRef({
|
|
142
|
+
handle: asString(row[opts.fileKey]),
|
|
143
|
+
raw: row
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
write(next, _prev, _ctx) {
|
|
147
|
+
return next.filter((r) => r.handle != null).map((r, i) => {
|
|
148
|
+
const base = r.raw && typeof r.raw === "object" ? { ...r.raw } : {};
|
|
149
|
+
base[opts.fileKey] = r.handle;
|
|
150
|
+
if (opts.sortKey) base[opts.sortKey] = i + 1;
|
|
151
|
+
return base;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
162
155
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
156
|
+
function asString(v) {
|
|
157
|
+
return typeof v === "string" ? v : void 0;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/contract/run-contract.ts
|
|
161
|
+
const single = {
|
|
162
|
+
resource: "demo",
|
|
163
|
+
field: "file",
|
|
164
|
+
multiple: false
|
|
165
|
+
};
|
|
166
|
+
const many = {
|
|
167
|
+
resource: "demo",
|
|
168
|
+
field: "files",
|
|
169
|
+
multiple: true
|
|
170
|
+
};
|
|
167
171
|
function runFileStorageContract(opts, runner) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
172
|
+
const { mapper } = opts;
|
|
173
|
+
const it = runner.it;
|
|
174
|
+
const expect = runner.expect;
|
|
175
|
+
it("declares a pipeline", () => {
|
|
176
|
+
expect([
|
|
177
|
+
"immediate",
|
|
178
|
+
"fused",
|
|
179
|
+
"handshake",
|
|
180
|
+
"resumable"
|
|
181
|
+
]).toContain(opts.adapter().pipeline);
|
|
182
|
+
});
|
|
183
|
+
it("ships a default mapper", () => {
|
|
184
|
+
const m = opts.adapter().defaultMapper;
|
|
185
|
+
expect(typeof m.read).toBe("function");
|
|
186
|
+
expect(typeof m.write).toBe("function");
|
|
187
|
+
});
|
|
188
|
+
if (opts.writesColumnPatch) {
|
|
189
|
+
it("emits an empty patch when nothing changed", () => {
|
|
190
|
+
const refs = mapper.read(opts.sampleColumn.multiple, many);
|
|
191
|
+
const out = mapper.write(refs, refs, many);
|
|
192
|
+
expect(Object.keys(out.keys ?? {}).length).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
it("emits only patch keys, never the raw column value", () => {
|
|
195
|
+
const refs = mapper.read(opts.sampleColumn.multiple, many);
|
|
196
|
+
const out = mapper.write(refs, refs, many);
|
|
197
|
+
expect(typeof out.keys).toBe("object");
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
it("round-trips an untouched single value unchanged", () => {
|
|
201
|
+
const refs = mapper.read(opts.sampleColumn.single, single);
|
|
202
|
+
expect(mapper.write(refs, refs, single)).toEqual(opts.sampleColumn.single);
|
|
203
|
+
});
|
|
204
|
+
it("round-trips an untouched multiple value unchanged", () => {
|
|
205
|
+
const refs = mapper.read(opts.sampleColumn.multiple, many);
|
|
206
|
+
expect(mapper.write(refs, refs, many)).toEqual(opts.sampleColumn.multiple);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
it("reads null as no files", () => {
|
|
210
|
+
expect(mapper.read(null, single)).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
it("gives every ref a distinct localId", () => {
|
|
213
|
+
const refs = mapper.read(opts.sampleColumn.multiple, many);
|
|
214
|
+
expect(new Set(refs.map((r) => r.localId)).size).toBe(refs.length);
|
|
215
|
+
});
|
|
216
|
+
if (!opts.skipUpload) {
|
|
217
|
+
it("reports progress and resolves to a ref carrying a handle", async () => {
|
|
218
|
+
const adapter = opts.adapter();
|
|
219
|
+
const seen = [];
|
|
220
|
+
const ref = await adapter.upload(new File(["hello"], "a.txt", { type: "text/plain" }), {
|
|
221
|
+
signal: new AbortController().signal,
|
|
222
|
+
onProgress: (loaded) => seen.push(loaded)
|
|
223
|
+
});
|
|
224
|
+
expect(ref.handle).toBeTruthy();
|
|
225
|
+
expect(seen.length).toBeGreaterThan(0);
|
|
226
|
+
expect(seen.at(-1)).toBe(5);
|
|
227
|
+
});
|
|
228
|
+
it("rejects with an AbortError when the signal is already aborted", async () => {
|
|
229
|
+
const adapter = opts.adapter();
|
|
230
|
+
const ac = new AbortController();
|
|
231
|
+
ac.abort();
|
|
232
|
+
await expect(adapter.upload(new File(["x"], "x.txt"), {
|
|
233
|
+
signal: ac.signal,
|
|
234
|
+
onProgress: () => {}
|
|
235
|
+
})).rejects.toThrow(/abort/i);
|
|
236
|
+
});
|
|
237
|
+
it("accepts a retry after a failure", async () => {
|
|
238
|
+
const adapter = opts.adapter();
|
|
239
|
+
const file = new File(["x"], "x.txt");
|
|
240
|
+
const aborted = new AbortController();
|
|
241
|
+
aborted.abort();
|
|
242
|
+
await expect(adapter.upload(file, {
|
|
243
|
+
signal: aborted.signal,
|
|
244
|
+
onProgress: () => {}
|
|
245
|
+
})).rejects.toThrow();
|
|
246
|
+
const second = await adapter.upload(file, {
|
|
247
|
+
signal: new AbortController().signal,
|
|
248
|
+
onProgress: () => {}
|
|
249
|
+
});
|
|
250
|
+
expect(second.handle).toBeTruthy();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
it("resolves a URL for a ref", async () => {
|
|
254
|
+
const resolved = await opts.adapter().resolveUrl({
|
|
255
|
+
localId: "x",
|
|
256
|
+
handle: "h"
|
|
257
|
+
}, opts.resolveCtx ?? {});
|
|
258
|
+
const url = typeof resolved === "string" ? resolved : resolved.url;
|
|
259
|
+
expect(url).toMatch(/^https?:\/\//);
|
|
260
|
+
});
|
|
261
|
+
it("either returns a permanent string or states a future expiry — never neither", async () => {
|
|
262
|
+
const resolved = await opts.adapter().resolveUrl({
|
|
263
|
+
localId: "x",
|
|
264
|
+
handle: "h"
|
|
265
|
+
}, opts.resolveCtx ?? {});
|
|
266
|
+
if (typeof resolved === "string") return;
|
|
267
|
+
expect(typeof resolved.expiresAt).toBe("number");
|
|
268
|
+
expect(resolved.expiresAt).toBeGreaterThan(Date.now());
|
|
269
|
+
});
|
|
270
|
+
it("leaves unreportable metadata absent rather than guessing", async () => {
|
|
271
|
+
const adapter = opts.adapter();
|
|
272
|
+
if (!adapter.stat) return;
|
|
273
|
+
const [out] = await adapter.stat([{
|
|
274
|
+
localId: "x",
|
|
275
|
+
handle: "definitely-not-stored"
|
|
276
|
+
}]);
|
|
277
|
+
expect(out?.size).not.toBe(0);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/contract/fake-adapter.ts
|
|
270
282
|
function createFakeAdapter(opts = {}) {
|
|
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
|
-
|
|
283
|
+
const stored = new Map();
|
|
284
|
+
const removed = [];
|
|
285
|
+
let seq = 0;
|
|
286
|
+
return {
|
|
287
|
+
stored,
|
|
288
|
+
removed,
|
|
289
|
+
pipeline: opts.pipeline ?? "immediate",
|
|
290
|
+
defaultMapper: scalarHandle(),
|
|
291
|
+
async upload(file, ctx) {
|
|
292
|
+
const ticks = opts.progressTicks ?? 2;
|
|
293
|
+
for (let i = 1; i <= ticks; i += 1) {
|
|
294
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
295
|
+
await Promise.resolve();
|
|
296
|
+
ctx.onProgress(Math.round(file.size * i / ticks), file.size);
|
|
297
|
+
}
|
|
298
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
299
|
+
if (opts.failWith) throw new Error(opts.failWith);
|
|
300
|
+
seq += 1;
|
|
301
|
+
const handle = `fake-${seq}-${file.name}`;
|
|
302
|
+
stored.set(handle, file);
|
|
303
|
+
return newRef({
|
|
304
|
+
handle,
|
|
305
|
+
name: file.name,
|
|
306
|
+
mime: file.type || void 0,
|
|
307
|
+
size: file.size
|
|
308
|
+
});
|
|
309
|
+
},
|
|
310
|
+
resolveUrl(ref, ctx) {
|
|
311
|
+
const q = ctx.variant?.width ? `?w=${ctx.variant.width}` : "";
|
|
312
|
+
const url = `https://fake.test/${ref.handle}${q}`;
|
|
313
|
+
if (opts.signedTtlMs == null) return url;
|
|
314
|
+
return Promise.resolve({
|
|
315
|
+
url,
|
|
316
|
+
expiresAt: Date.now() + opts.signedTtlMs
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
async stat(refs) {
|
|
320
|
+
return refs.map((r) => {
|
|
321
|
+
const f = r.handle ? stored.get(r.handle) : void 0;
|
|
322
|
+
return f ? {
|
|
323
|
+
...r,
|
|
324
|
+
name: f.name,
|
|
325
|
+
size: f.size,
|
|
326
|
+
mime: f.type || void 0
|
|
327
|
+
} : r;
|
|
328
|
+
});
|
|
329
|
+
},
|
|
330
|
+
async remove(ref) {
|
|
331
|
+
if (ref.handle) {
|
|
332
|
+
stored.delete(ref.handle);
|
|
333
|
+
removed.push(ref.handle);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/adapters/inline.ts
|
|
315
340
|
function inlineFileStorage(opts) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// src/adapters/nhost.ts
|
|
341
|
+
return {
|
|
342
|
+
pipeline: "fused",
|
|
343
|
+
encoding: opts.encoding,
|
|
344
|
+
defaultMapper: scalarHandle(),
|
|
345
|
+
orphans: "keep",
|
|
346
|
+
async upload(file, _ctx) {
|
|
347
|
+
return newRef({
|
|
348
|
+
pending: file,
|
|
349
|
+
name: file.name,
|
|
350
|
+
mime: file.type || void 0,
|
|
351
|
+
size: file.size
|
|
352
|
+
});
|
|
353
|
+
},
|
|
354
|
+
resolveUrl(ref, ctx) {
|
|
355
|
+
return opts.resolveUrl(ref, ctx);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/adapters/nhost.ts
|
|
337
361
|
function nhostFileStorage(opts) {
|
|
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
|
-
if (!res.ok && res.status !== 404) {
|
|
423
|
-
throw new Error(`nhost delete failed: ${res.status}`);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// src/adapters/supabase.ts
|
|
362
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
363
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
364
|
+
async function authHeaders() {
|
|
365
|
+
const token = await opts.getToken();
|
|
366
|
+
if (!token) throw new Error("nhostFileStorage: no access token — the user is not signed in");
|
|
367
|
+
return { Authorization: `Bearer ${token}` };
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
pipeline: "immediate",
|
|
371
|
+
defaultMapper: scalarHandle(),
|
|
372
|
+
orphans: opts.orphans ?? "best-effort",
|
|
373
|
+
async upload(file, ctx) {
|
|
374
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
375
|
+
const headers = await authHeaders();
|
|
376
|
+
const fd = new FormData();
|
|
377
|
+
fd.append("file[]", file);
|
|
378
|
+
if (opts.bucketId) fd.append("bucket-id", opts.bucketId);
|
|
379
|
+
ctx.onProgress(0, file.size);
|
|
380
|
+
const res = await fetchImpl(`${base}/v1/files`, {
|
|
381
|
+
method: "POST",
|
|
382
|
+
headers,
|
|
383
|
+
body: fd,
|
|
384
|
+
signal: ctx.signal
|
|
385
|
+
});
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
const body = await res.text().catch(() => "");
|
|
388
|
+
throw new Error(`nhost upload failed: ${res.status} ${body}`.trim());
|
|
389
|
+
}
|
|
390
|
+
const json = await res.json();
|
|
391
|
+
const meta = "processedFiles" in json ? json.processedFiles?.[0] : json;
|
|
392
|
+
if (!meta?.id) throw new Error("nhost upload returned no file id");
|
|
393
|
+
ctx.onProgress(file.size, file.size);
|
|
394
|
+
return newRef({
|
|
395
|
+
handle: meta.id,
|
|
396
|
+
name: meta.name ?? file.name,
|
|
397
|
+
mime: meta.mimeType ?? file.type ?? void 0,
|
|
398
|
+
size: meta.size ?? file.size
|
|
399
|
+
});
|
|
400
|
+
},
|
|
401
|
+
async resolveUrl(ref, ctx) {
|
|
402
|
+
const headers = await authHeaders();
|
|
403
|
+
const params = new URLSearchParams();
|
|
404
|
+
if (ctx.variant?.width) params.set("w", String(ctx.variant.width));
|
|
405
|
+
if (ctx.variant?.height) params.set("h", String(ctx.variant.height));
|
|
406
|
+
const qs = params.toString();
|
|
407
|
+
const res = await fetchImpl(`${base}/v1/files/${ref.handle}/presignedurl${qs ? `?${qs}` : ""}`, { headers });
|
|
408
|
+
if (!res.ok) throw new Error(`nhost presign failed: ${res.status}`);
|
|
409
|
+
const json = await res.json();
|
|
410
|
+
if (!json.url) throw new Error("nhost presign returned no url");
|
|
411
|
+
return {
|
|
412
|
+
url: json.url,
|
|
413
|
+
expiresAt: Date.now() + (json.expiration ?? 1800) * 1e3
|
|
414
|
+
};
|
|
415
|
+
},
|
|
416
|
+
async stat(refs) {
|
|
417
|
+
const headers = await authHeaders();
|
|
418
|
+
return Promise.all(refs.map(async (ref) => {
|
|
419
|
+
const res = await fetchImpl(`${base}/v1/files/${ref.handle}`, {
|
|
420
|
+
method: "HEAD",
|
|
421
|
+
headers
|
|
422
|
+
});
|
|
423
|
+
if (!res.ok) return ref;
|
|
424
|
+
const rawSize = res.headers.get("content-length");
|
|
425
|
+
const size = rawSize == null ? NaN : Number(rawSize);
|
|
426
|
+
const mime = res.headers.get("content-type") ?? void 0;
|
|
427
|
+
return {
|
|
428
|
+
...ref,
|
|
429
|
+
...Number.isFinite(size) ? { size } : {},
|
|
430
|
+
...mime ? { mime } : {}
|
|
431
|
+
};
|
|
432
|
+
}));
|
|
433
|
+
},
|
|
434
|
+
async remove(ref) {
|
|
435
|
+
const headers = await authHeaders();
|
|
436
|
+
const res = await fetchImpl(`${base}/v1/files/${ref.handle}`, {
|
|
437
|
+
method: "DELETE",
|
|
438
|
+
headers
|
|
439
|
+
});
|
|
440
|
+
if (!res.ok && res.status !== 404) throw new Error(`nhost delete failed: ${res.status}`);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
445
|
+
//#region src/adapters/supabase.ts
|
|
430
446
|
function pathStamp() {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
return Math.random().toString(36).slice(2, 10).padEnd(8, "0");
|
|
447
|
+
if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID().slice(0, 8);
|
|
448
|
+
return Math.random().toString(36).slice(2, 10).padEnd(8, "0");
|
|
435
449
|
}
|
|
436
450
|
function supabaseFileStorage(opts) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
// src/adapters/s3.ts
|
|
451
|
+
const bucket = () => opts.client.storage.from(opts.bucket);
|
|
452
|
+
const ttl = opts.signedTtlSec ?? 3600;
|
|
453
|
+
return {
|
|
454
|
+
pipeline: "immediate",
|
|
455
|
+
defaultMapper: scalarHandle(),
|
|
456
|
+
orphans: opts.orphans ?? "best-effort",
|
|
457
|
+
async upload(file, ctx) {
|
|
458
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
459
|
+
const stamp = `${pathStamp()}-${file.name}`;
|
|
460
|
+
const path = opts.pathPrefix ? `${opts.pathPrefix}/${stamp}` : stamp;
|
|
461
|
+
ctx.onProgress(0, file.size);
|
|
462
|
+
const { data, error } = await bucket().upload(path, file, { contentType: file.type || void 0 });
|
|
463
|
+
if (error) throw new Error(`Supabase upload failed: ${error.message}`);
|
|
464
|
+
if (!data) throw new Error("Supabase upload returned no path");
|
|
465
|
+
ctx.onProgress(file.size, file.size);
|
|
466
|
+
return newRef({
|
|
467
|
+
handle: data.path,
|
|
468
|
+
name: file.name,
|
|
469
|
+
mime: file.type || void 0,
|
|
470
|
+
size: file.size
|
|
471
|
+
});
|
|
472
|
+
},
|
|
473
|
+
async resolveUrl(ref, _ctx) {
|
|
474
|
+
const path = ref.handle;
|
|
475
|
+
if (opts.access === "public") return bucket().getPublicUrl(path).data.publicUrl;
|
|
476
|
+
const { data, error } = await bucket().createSignedUrl(path, ttl);
|
|
477
|
+
if (error) throw new Error(`Supabase sign failed: ${error.message}`);
|
|
478
|
+
if (!data) throw new Error("Supabase sign returned no url");
|
|
479
|
+
return {
|
|
480
|
+
url: data.signedUrl,
|
|
481
|
+
expiresAt: Date.now() + ttl * 1e3
|
|
482
|
+
};
|
|
483
|
+
},
|
|
484
|
+
async remove(ref) {
|
|
485
|
+
const { error } = await bucket().remove([ref.handle]);
|
|
486
|
+
if (error) throw new Error(`Supabase remove failed: ${error.message}`);
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
//#endregion
|
|
491
|
+
//#region src/adapters/s3.ts
|
|
479
492
|
function s3FileStorage(opts) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// src/mappers/column-patch.ts
|
|
548
|
-
var COLUMN_PATCH = /* @__PURE__ */ Symbol.for("buro.fileStorage.columnPatch");
|
|
493
|
+
return {
|
|
494
|
+
pipeline: "handshake",
|
|
495
|
+
defaultMapper: scalarHandle(),
|
|
496
|
+
orphans: opts.orphans ?? "best-effort",
|
|
497
|
+
async upload(file, ctx) {
|
|
498
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
499
|
+
const target = await opts.sign({
|
|
500
|
+
name: file.name,
|
|
501
|
+
type: file.type,
|
|
502
|
+
size: file.size
|
|
503
|
+
});
|
|
504
|
+
if (ctx.signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
505
|
+
const xhr = (opts.xhrImpl ?? (() => new XMLHttpRequest()))();
|
|
506
|
+
const method = target.method ?? "PUT";
|
|
507
|
+
await new Promise((resolve, reject) => {
|
|
508
|
+
const onAbort = () => xhr.abort();
|
|
509
|
+
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
510
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
511
|
+
const ev = e;
|
|
512
|
+
ctx.onProgress(ev.loaded, ev.total || file.size);
|
|
513
|
+
});
|
|
514
|
+
xhr.addEventListener("load", () => {
|
|
515
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
516
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
517
|
+
ctx.onProgress(file.size, file.size);
|
|
518
|
+
resolve();
|
|
519
|
+
} else reject(new Error(`S3 upload failed: ${xhr.status}`));
|
|
520
|
+
});
|
|
521
|
+
xhr.addEventListener("error", () => {
|
|
522
|
+
ctx.signal.removeEventListener("abort", onAbort);
|
|
523
|
+
reject(new Error("S3 upload failed: network error"));
|
|
524
|
+
});
|
|
525
|
+
xhr.addEventListener("abort", () => {
|
|
526
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
527
|
+
});
|
|
528
|
+
xhr.open(method, target.url);
|
|
529
|
+
for (const [k, v] of Object.entries(target.headers ?? {})) xhr.setRequestHeader(k, v);
|
|
530
|
+
if (method === "POST") {
|
|
531
|
+
const fd = new FormData();
|
|
532
|
+
for (const [k, v] of Object.entries(target.fields ?? {})) fd.append(k, v);
|
|
533
|
+
fd.append("file", file);
|
|
534
|
+
xhr.send(fd);
|
|
535
|
+
} else xhr.send(file);
|
|
536
|
+
});
|
|
537
|
+
return newRef({
|
|
538
|
+
handle: target.key,
|
|
539
|
+
name: file.name,
|
|
540
|
+
mime: file.type || void 0,
|
|
541
|
+
size: file.size
|
|
542
|
+
});
|
|
543
|
+
},
|
|
544
|
+
resolveUrl(ref, ctx) {
|
|
545
|
+
const key = ref.handle;
|
|
546
|
+
if (opts.signView) return opts.signView(key, ctx);
|
|
547
|
+
if (opts.publicBaseUrl) return `${opts.publicBaseUrl.replace(/\/+$/, "")}/${key}`;
|
|
548
|
+
throw new Error("s3FileStorage: pass publicBaseUrl for a public bucket or signView for a private one");
|
|
549
|
+
},
|
|
550
|
+
...opts.remove ? { async remove(ref) {
|
|
551
|
+
await opts.remove?.(ref.handle);
|
|
552
|
+
} } : {}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/mappers/column-patch.ts
|
|
557
|
+
const COLUMN_PATCH = Symbol.for("buro.fileStorage.columnPatch");
|
|
549
558
|
function columnPatch(keys) {
|
|
550
|
-
|
|
559
|
+
return {
|
|
560
|
+
[COLUMN_PATCH]: true,
|
|
561
|
+
keys
|
|
562
|
+
};
|
|
551
563
|
}
|
|
552
564
|
function isColumnPatch(value) {
|
|
553
|
-
|
|
565
|
+
return typeof value === "object" && value !== null && COLUMN_PATCH in value;
|
|
554
566
|
}
|
|
555
|
-
|
|
556
|
-
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/adapters/pocketbase.ts
|
|
557
569
|
function pocketbaseDiff() {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
570
|
+
return {
|
|
571
|
+
carriesPending: true,
|
|
572
|
+
read(column) {
|
|
573
|
+
if (column == null) return [];
|
|
574
|
+
return (Array.isArray(column) ? column : [column]).filter((n) => typeof n === "string" && n !== "").map((name) => newRef({
|
|
575
|
+
handle: name,
|
|
576
|
+
raw: name,
|
|
577
|
+
name
|
|
578
|
+
}));
|
|
579
|
+
},
|
|
580
|
+
write(next, prev, ctx) {
|
|
581
|
+
const added = next.filter((r) => r.pending).map((r) => r.pending);
|
|
582
|
+
if (!ctx.multiple) {
|
|
583
|
+
if (added.length > 0) return columnPatch({ [ctx.field]: added[0] });
|
|
584
|
+
if (next.find((r) => r.handle)) return columnPatch({});
|
|
585
|
+
return prev.length > 0 ? columnPatch({ [ctx.field]: "" }) : columnPatch({});
|
|
586
|
+
}
|
|
587
|
+
const keptHandles = new Set(next.filter((r) => r.handle).map((r) => r.handle));
|
|
588
|
+
const removed = prev.map((r) => r.handle).filter((h) => h != null && !keptHandles.has(h));
|
|
589
|
+
const keys = {};
|
|
590
|
+
if (added.length > 0) keys[`${ctx.field}+`] = added;
|
|
591
|
+
if (removed.length > 0) keys[`${ctx.field}-`] = removed;
|
|
592
|
+
return columnPatch(keys);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
581
595
|
}
|
|
582
596
|
function pocketbaseFileStorage(opts) {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
const qs = params.toString();
|
|
627
|
-
return `${base}/api/files/${ctx.resource}/${recordId}/${ref.handle}${qs ? `?${qs}` : ""}`;
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/wrapper/encode.ts
|
|
597
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
598
|
+
const ttl = opts.tokenTtlSec ?? 120;
|
|
599
|
+
return {
|
|
600
|
+
pipeline: "fused",
|
|
601
|
+
encoding: "multipart",
|
|
602
|
+
defaultMapper: pocketbaseDiff(),
|
|
603
|
+
orphans: "keep",
|
|
604
|
+
async upload(file, _ctx) {
|
|
605
|
+
return newRef({
|
|
606
|
+
pending: file,
|
|
607
|
+
name: file.name,
|
|
608
|
+
mime: file.type || void 0,
|
|
609
|
+
size: file.size
|
|
610
|
+
});
|
|
611
|
+
},
|
|
612
|
+
async resolveUrl(ref, ctx) {
|
|
613
|
+
const recordId = ctx.record?.id;
|
|
614
|
+
if (!ctx.resource || !recordId) throw new Error("pocketbaseFileStorage: a file URL needs its collection and record id — the filename alone does not identify a file in PocketBase.");
|
|
615
|
+
const params = new URLSearchParams();
|
|
616
|
+
if (ctx.variant?.width || ctx.variant?.height) {
|
|
617
|
+
const w = ctx.variant.width ?? 0;
|
|
618
|
+
const h = ctx.variant.height ?? 0;
|
|
619
|
+
params.set("thumb", `${w}x${h}f`);
|
|
620
|
+
}
|
|
621
|
+
if (ctx.disposition === "attachment") params.set("download", "1");
|
|
622
|
+
if (opts.protectedFiles) {
|
|
623
|
+
if (!opts.getFileToken) throw new Error("pocketbaseFileStorage: protectedFiles is set but getFileToken was not supplied");
|
|
624
|
+
const token = await opts.getFileToken();
|
|
625
|
+
params.set("token", token);
|
|
626
|
+
const qs = params.toString();
|
|
627
|
+
return {
|
|
628
|
+
url: `${base}/api/files/${ctx.resource}/${recordId}/${ref.handle}${qs ? `?${qs}` : ""}`,
|
|
629
|
+
expiresAt: Date.now() + ttl * 1e3
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const qs = params.toString();
|
|
633
|
+
return `${base}/api/files/${ctx.resource}/${recordId}/${ref.handle}${qs ? `?${qs}` : ""}`;
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/wrapper/encode.ts
|
|
633
639
|
function splicePatches(variables) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
function containsFile(value, seen =
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
function containsColumnPatch(value, seen =
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return false;
|
|
666
|
-
}
|
|
640
|
+
let touched = false;
|
|
641
|
+
const out = {};
|
|
642
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
643
|
+
if (isColumnPatch(value)) {
|
|
644
|
+
touched = true;
|
|
645
|
+
Object.assign(out, value.keys);
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
out[key] = value;
|
|
649
|
+
}
|
|
650
|
+
return touched ? out : variables;
|
|
651
|
+
}
|
|
652
|
+
function containsFile(value, seen = new WeakSet()) {
|
|
653
|
+
if (typeof File !== "undefined" && value instanceof File) return true;
|
|
654
|
+
if (!value || typeof value !== "object") return false;
|
|
655
|
+
if (seen.has(value)) return false;
|
|
656
|
+
seen.add(value);
|
|
657
|
+
if (Array.isArray(value)) return value.some((v) => containsFile(v, seen));
|
|
658
|
+
return Object.values(value).some((v) => containsFile(v, seen));
|
|
659
|
+
}
|
|
660
|
+
function containsColumnPatch(value, seen = new WeakSet()) {
|
|
661
|
+
if (!value || typeof value !== "object") return false;
|
|
662
|
+
try {
|
|
663
|
+
if (isColumnPatch(value)) return true;
|
|
664
|
+
if (seen.has(value)) return false;
|
|
665
|
+
seen.add(value);
|
|
666
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
667
|
+
return Object.entries(descriptors).some(([key, descriptor]) => key !== "length" && "value" in descriptor && containsColumnPatch(descriptor.value, seen));
|
|
668
|
+
} catch {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
667
671
|
}
|
|
668
672
|
async function toDataUrl(file) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
673
|
+
const buf = await file.arrayBuffer();
|
|
674
|
+
const bytes = new Uint8Array(buf);
|
|
675
|
+
let binary = "";
|
|
676
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
677
|
+
const b64 = typeof btoa === "function" ? btoa(binary) : Buffer.from(bytes).toString("base64");
|
|
678
|
+
return `data:${file.type || "application/octet-stream"};base64,${b64}`;
|
|
675
679
|
}
|
|
676
680
|
async function encodePayload(variables, plan) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
// src/wrapper/with-file-storage.ts
|
|
723
|
-
var FILE_STORAGE_WRAPPED = /* @__PURE__ */ Symbol.for("buro.fileStorage.wrapped");
|
|
724
|
-
var MUTATING = /* @__PURE__ */ new Set(["create", "update", "createMany", "updateMany"]);
|
|
681
|
+
const staged = {};
|
|
682
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
683
|
+
if (encodingFor(key, plan) !== "base64") {
|
|
684
|
+
staged[key] = value;
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (value instanceof File) staged[key] = await toDataUrl(value);
|
|
688
|
+
else if (Array.isArray(value)) staged[key] = await Promise.all(value.map((v) => v instanceof File ? toDataUrl(v) : Promise.resolve(v)));
|
|
689
|
+
else staged[key] = value;
|
|
690
|
+
}
|
|
691
|
+
if (!containsFile(staged)) return staged;
|
|
692
|
+
const encoding = plan.bodyEncoding;
|
|
693
|
+
const fd = new FormData();
|
|
694
|
+
if (encoding === "graphql-upload") {
|
|
695
|
+
const files = [];
|
|
696
|
+
const map = {};
|
|
697
|
+
const scrubbed = {};
|
|
698
|
+
for (const [key, value] of Object.entries(staged)) if (value instanceof File) {
|
|
699
|
+
map[String(files.length)] = [`variables.${key}`];
|
|
700
|
+
files.push(value);
|
|
701
|
+
scrubbed[key] = null;
|
|
702
|
+
} else scrubbed[key] = value;
|
|
703
|
+
fd.append("operations", JSON.stringify({ variables: scrubbed }));
|
|
704
|
+
fd.append("map", JSON.stringify(map));
|
|
705
|
+
files.forEach((f, i) => {
|
|
706
|
+
fd.append(String(i), f);
|
|
707
|
+
});
|
|
708
|
+
return fd;
|
|
709
|
+
}
|
|
710
|
+
for (const [key, value] of Object.entries(staged)) if (value instanceof File) fd.append(key, value);
|
|
711
|
+
else if (Array.isArray(value)) for (const item of value) fd.append(key, item instanceof File ? item : String(item));
|
|
712
|
+
else if (value == null) fd.append(key, "");
|
|
713
|
+
else if (typeof value === "object") fd.append(key, JSON.stringify(value));
|
|
714
|
+
else fd.append(key, String(value));
|
|
715
|
+
return fd;
|
|
716
|
+
}
|
|
717
|
+
//#endregion
|
|
718
|
+
//#region src/wrapper/with-file-storage.ts
|
|
719
|
+
const FILE_STORAGE_WRAPPED = Symbol.for("buro.fileStorage.wrapped");
|
|
720
|
+
const MUTATING = new Set([
|
|
721
|
+
"create",
|
|
722
|
+
"update",
|
|
723
|
+
"updateMany"
|
|
724
|
+
]);
|
|
725
725
|
function isProviderMap(value) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
726
|
+
if (!value || typeof value !== "object") return false;
|
|
727
|
+
const entries = Object.values(value);
|
|
728
|
+
return entries.length > 0 && entries.every((v) => v && typeof v === "object" && !("then" in v));
|
|
729
729
|
}
|
|
730
730
|
function withFileStorage(provider, app) {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
775
|
-
Object.defineProperty(wrapped, FILE_STORAGE_WRAPPED, { value: true, enumerable: false });
|
|
776
|
-
return wrapped;
|
|
731
|
+
const registry = app.getService(FILE_STORAGE_SERVICE);
|
|
732
|
+
if (!registry) throw new Error("withFileStorage: no file storage registered — add fileStoragePlugin({...}) to defineApp");
|
|
733
|
+
if (isProviderMap(provider) && !provider.getList) {
|
|
734
|
+
const out = {};
|
|
735
|
+
for (const [name, entry] of Object.entries(provider)) out[name] = withFileStorage(entry, app);
|
|
736
|
+
Object.defineProperty(out, FILE_STORAGE_WRAPPED, {
|
|
737
|
+
value: true,
|
|
738
|
+
enumerable: false
|
|
739
|
+
});
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
const source = provider;
|
|
743
|
+
const wrapped = Object.create(Object.getPrototypeOf(source));
|
|
744
|
+
for (const key of allKeys(source)) {
|
|
745
|
+
const value = source[key];
|
|
746
|
+
if (typeof value !== "function" || !MUTATING.has(key)) {
|
|
747
|
+
wrapped[key] = value;
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
wrapped[key] = async (args) => {
|
|
751
|
+
const variables = args?.variables;
|
|
752
|
+
if (!variables || typeof variables !== "object" || variables instanceof FormData) return value(args);
|
|
753
|
+
if (key === "updateMany" && (containsFile(variables) || containsColumnPatch(variables))) throw new Error(`withFileStorage: bulk file mutations are not supported for ${key}; submit one record at a time so FileMapper patches and request-body encoding stay unambiguous.`);
|
|
754
|
+
const spliced = splicePatches(variables);
|
|
755
|
+
if (!containsFile(spliced)) {
|
|
756
|
+
const passthrough = spliced === variables ? args : {
|
|
757
|
+
...args,
|
|
758
|
+
variables: spliced
|
|
759
|
+
};
|
|
760
|
+
return value(passthrough);
|
|
761
|
+
}
|
|
762
|
+
const encoded = await encodePayload(spliced, planFor(app, registry, String(args.resource)));
|
|
763
|
+
return value({
|
|
764
|
+
...args,
|
|
765
|
+
variables: encoded
|
|
766
|
+
});
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
Object.defineProperty(wrapped, FILE_STORAGE_WRAPPED, {
|
|
770
|
+
value: true,
|
|
771
|
+
enumerable: false
|
|
772
|
+
});
|
|
773
|
+
return wrapped;
|
|
777
774
|
}
|
|
778
775
|
function allKeys(source) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
776
|
+
const keys = new Set(Object.keys(source));
|
|
777
|
+
const proto = Object.getPrototypeOf(source);
|
|
778
|
+
if (proto && proto !== Object.prototype) {
|
|
779
|
+
for (const k of Object.getOwnPropertyNames(proto)) if (k !== "constructor") keys.add(k);
|
|
780
|
+
}
|
|
781
|
+
return [...keys];
|
|
785
782
|
}
|
|
786
783
|
function fieldOfKey(key) {
|
|
787
|
-
|
|
784
|
+
return key.endsWith("+") || key.endsWith("-") ? key.slice(0, -1) : key;
|
|
788
785
|
}
|
|
789
786
|
function encodingFor(key, plan) {
|
|
790
|
-
|
|
787
|
+
return plan.knownEncodings.get(fieldOfKey(key)) ?? plan.defaultEncoding;
|
|
791
788
|
}
|
|
792
789
|
function planFor(app, registry, resource) {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
790
|
+
const knownEncodings = new Map();
|
|
791
|
+
const defaultAdapter = registry.has("default") ? registry.get("default") : void 0;
|
|
792
|
+
const defaultEncoding = defaultAdapter?.pipeline === "fused" && defaultAdapter.encoding ? defaultAdapter.encoding : "multipart";
|
|
793
|
+
let bodyEncoding = defaultEncoding === "base64" ? "multipart" : defaultEncoding;
|
|
794
|
+
const def = app.registry.get(resource);
|
|
795
|
+
for (const [fieldName, field] of Object.entries(def?.fields ?? {})) {
|
|
796
|
+
if (field.kind !== "file") continue;
|
|
797
|
+
const ui = field.ui;
|
|
798
|
+
if (!registry.has(ui?.fileStorage ?? "default")) continue;
|
|
799
|
+
const adapter = registry.get(ui?.fileStorage);
|
|
800
|
+
if (adapter.pipeline !== "fused" || !adapter.encoding) continue;
|
|
801
|
+
knownEncodings.set(fieldName, adapter.encoding);
|
|
802
|
+
if (adapter.encoding !== "base64") bodyEncoding = adapter.encoding;
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
knownEncodings,
|
|
806
|
+
defaultEncoding,
|
|
807
|
+
bodyEncoding
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
//#endregion
|
|
811
|
+
//#region src/wrapper/assert-wiring.ts
|
|
812
|
+
const BODY_ENCODINGS = ["multipart", "graphql-upload"];
|
|
812
813
|
function assertFileStorageWiring(app, provider) {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const entries = [...seen.entries()];
|
|
844
|
-
const [encA, fieldA] = entries[0];
|
|
845
|
-
const [encB, fieldB] = entries[1];
|
|
846
|
-
throw new Error(
|
|
847
|
-
`Resource '${def.name}' mixes two request-body encodings: field '${fieldA}' wants ${encA} and field '${fieldB}' wants ${encB}. One request has one body \u2014 move one of these fields to a storage using the same encoding, or to base64, which transforms values rather than claiming the body.`
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// src/baseline-carrier.ts
|
|
855
|
-
var FILE_MAPPER_BASELINE_CARRIER = /* @__PURE__ */ Symbol.for("buro.fileMapper.baselineCarrier");
|
|
814
|
+
const registry = app.getService(FILE_STORAGE_SERVICE);
|
|
815
|
+
if (!registry) return;
|
|
816
|
+
if (registry.hasFused()) {
|
|
817
|
+
if (!(provider != null && (typeof provider === "object" || typeof provider === "function") && FILE_STORAGE_WRAPPED in provider)) throw new Error("A fused file storage is registered, but the data provider is not wrapped. Files ride inside the record mutation for this pipeline, so an unwrapped provider will JSON.stringify them into `{}` and lose them silently. Wrap it once: dataProvider: withFileStorage(myDataProvider, app)");
|
|
818
|
+
}
|
|
819
|
+
for (const def of app.registry.list()) {
|
|
820
|
+
const seen = new Map();
|
|
821
|
+
for (const [fieldName, field] of Object.entries(def.fields ?? {})) {
|
|
822
|
+
if (field.kind !== "file") continue;
|
|
823
|
+
const ui = field.ui;
|
|
824
|
+
if (!registry.has(ui?.fileStorage ?? "default")) continue;
|
|
825
|
+
const adapter = registry.get(ui?.fileStorage);
|
|
826
|
+
if (adapter.pipeline === "fused") {
|
|
827
|
+
if (!(ui?.mapper ?? adapter.defaultMapper).carriesPending) throw new Error(`Resource '${def.name}' field '${fieldName}' pairs a fused file storage with a mapper that cannot carry a pending file forward. A ref on a fused pipeline never has a \`handle\` before the record mutation runs — only \`pending\` — and this mapper's \`write\` has no \`carriesPending\` marker, meaning it drops any ref with no handle. That silently discards every file added through this field, with no error. Use a mapper built for this pipeline (scalarHandle, pocketbaseDiff), or give this field one that sets \`carriesPending: true\` via \`f.file({ mapper })\`.`);
|
|
828
|
+
}
|
|
829
|
+
if (adapter.pipeline !== "fused" || !adapter.encoding) continue;
|
|
830
|
+
if (!BODY_ENCODINGS.includes(adapter.encoding)) continue;
|
|
831
|
+
if (seen.get(adapter.encoding) == null) seen.set(adapter.encoding, fieldName);
|
|
832
|
+
if (seen.size > 1) {
|
|
833
|
+
const entries = [...seen.entries()];
|
|
834
|
+
const [encA, fieldA] = entries[0];
|
|
835
|
+
const [encB, fieldB] = entries[1];
|
|
836
|
+
throw new Error(`Resource '${def.name}' mixes two request-body encodings: field '${fieldA}' wants ${encA} and field '${fieldB}' wants ${encB}. One request has one body — move one of these fields to a storage using the same encoding, or to base64, which transforms values rather than claiming the body.`);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
//#endregion
|
|
842
|
+
//#region src/baseline-carrier.ts
|
|
843
|
+
const FILE_MAPPER_BASELINE_CARRIER = Symbol.for("buro.fileMapper.baselineCarrier");
|
|
856
844
|
function inheritFileMapperBaseline(source, target) {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
var PACKAGE_NAME = "@burojs/file-storage";
|
|
877
|
-
|
|
845
|
+
let descriptor;
|
|
846
|
+
try {
|
|
847
|
+
descriptor = Object.getOwnPropertyDescriptor(source, FILE_MAPPER_BASELINE_CARRIER);
|
|
848
|
+
} catch {
|
|
849
|
+
return target;
|
|
850
|
+
}
|
|
851
|
+
if (descriptor === void 0 || !("value" in descriptor) || descriptor.value === null || typeof descriptor.value !== "object") return target;
|
|
852
|
+
Object.defineProperty(target, FILE_MAPPER_BASELINE_CARRIER, {
|
|
853
|
+
value: descriptor.value,
|
|
854
|
+
enumerable: false,
|
|
855
|
+
configurable: true,
|
|
856
|
+
writable: false
|
|
857
|
+
});
|
|
858
|
+
return target;
|
|
859
|
+
}
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/index.ts
|
|
862
|
+
const PACKAGE_NAME = "@burojs/file-storage";
|
|
863
|
+
//#endregion
|
|
878
864
|
exports.COLUMN_PATCH = COLUMN_PATCH;
|
|
879
865
|
exports.FILE_STORAGE_SERVICE = FILE_STORAGE_SERVICE;
|
|
880
866
|
exports.FILE_STORAGE_WRAPPED = FILE_STORAGE_WRAPPED;
|
|
@@ -903,5 +889,5 @@ exports.scalarHandle = scalarHandle;
|
|
|
903
889
|
exports.splicePatches = splicePatches;
|
|
904
890
|
exports.supabaseFileStorage = supabaseFileStorage;
|
|
905
891
|
exports.withFileStorage = withFileStorage;
|
|
906
|
-
|
|
892
|
+
|
|
907
893
|
//# sourceMappingURL=index.cjs.map
|