@richardmcquiston01/online-catalog-cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +387 -0
- package/dist/index.cjs +1550 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +558 -0
- package/dist/index.d.ts +558 -0
- package/dist/index.js +1527 -0
- package/dist/index.js.map +1 -0
- package/package.json +78 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1527 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, createWriteStream, unlinkSync } from 'fs';
|
|
2
|
+
import { join, resolve, extname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import { pipeline } from 'stream/promises';
|
|
7
|
+
|
|
8
|
+
// src/rich-text/builders.ts
|
|
9
|
+
function text(content, options = {}) {
|
|
10
|
+
return { type: "text", text: content, ...options };
|
|
11
|
+
}
|
|
12
|
+
function link(href, children) {
|
|
13
|
+
return { type: "link", href, children };
|
|
14
|
+
}
|
|
15
|
+
function paragraph(children, cssClass) {
|
|
16
|
+
return { type: "paragraph", children, ...cssClass ? { cssClass } : {} };
|
|
17
|
+
}
|
|
18
|
+
function heading(level, children, cssClass) {
|
|
19
|
+
return {
|
|
20
|
+
type: "heading",
|
|
21
|
+
level,
|
|
22
|
+
children,
|
|
23
|
+
...cssClass ? { cssClass } : {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function image(src, alt, cssClass) {
|
|
27
|
+
return { type: "image", src, alt, ...cssClass ? { cssClass } : {} };
|
|
28
|
+
}
|
|
29
|
+
function unorderedList(items, cssClass) {
|
|
30
|
+
return {
|
|
31
|
+
type: "list",
|
|
32
|
+
ordered: false,
|
|
33
|
+
items,
|
|
34
|
+
...cssClass ? { cssClass } : {}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function orderedList(items, cssClass) {
|
|
38
|
+
return {
|
|
39
|
+
type: "list",
|
|
40
|
+
ordered: true,
|
|
41
|
+
items,
|
|
42
|
+
...cssClass ? { cssClass } : {}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function blockquote(children, cssClass) {
|
|
46
|
+
return {
|
|
47
|
+
type: "blockquote",
|
|
48
|
+
children,
|
|
49
|
+
...cssClass ? { cssClass } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function document(nodes) {
|
|
53
|
+
return { version: 1, nodes };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// src/rich-text/validate.ts
|
|
57
|
+
function isObject(value) {
|
|
58
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
function isInlineNode(value) {
|
|
61
|
+
if (!isObject(value)) return false;
|
|
62
|
+
if (value.type === "text") {
|
|
63
|
+
return typeof value.text === "string";
|
|
64
|
+
}
|
|
65
|
+
if (value.type === "link") {
|
|
66
|
+
return typeof value.href === "string" && Array.isArray(value.children) && value.children.every(isInlineNode);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
function isRichTextNode(value) {
|
|
71
|
+
if (!isObject(value)) return false;
|
|
72
|
+
const type = value.type;
|
|
73
|
+
if (type === "paragraph" || type === "blockquote") {
|
|
74
|
+
return Array.isArray(value.children) && value.children.every(isInlineNode);
|
|
75
|
+
}
|
|
76
|
+
if (type === "heading") {
|
|
77
|
+
const level = value.level;
|
|
78
|
+
return typeof level === "number" && level >= 1 && level <= 6 && Array.isArray(value.children) && value.children.every(isInlineNode);
|
|
79
|
+
}
|
|
80
|
+
if (type === "image") {
|
|
81
|
+
return typeof value.src === "string" && typeof value.alt === "string";
|
|
82
|
+
}
|
|
83
|
+
if (type === "list") {
|
|
84
|
+
return typeof value.ordered === "boolean" && Array.isArray(value.items) && value.items.every(
|
|
85
|
+
(item) => Array.isArray(item) && item.every(isInlineNode)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
function isRichTextDocument(value) {
|
|
91
|
+
if (!isObject(value)) return false;
|
|
92
|
+
if (value.version !== 1) return false;
|
|
93
|
+
if (!Array.isArray(value.nodes)) return false;
|
|
94
|
+
return value.nodes.every(isRichTextNode);
|
|
95
|
+
}
|
|
96
|
+
function assertRichTextDocument(value) {
|
|
97
|
+
if (!isRichTextDocument(value)) {
|
|
98
|
+
throw new TypeError(
|
|
99
|
+
"Invalid RichTextDocument: must be { version: 1, nodes: RichTextNode[] }"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/installer/Installer.ts
|
|
105
|
+
var Installer = class {
|
|
106
|
+
constructor(adapter) {
|
|
107
|
+
this.adapter = adapter;
|
|
108
|
+
}
|
|
109
|
+
adapter;
|
|
110
|
+
/**
|
|
111
|
+
* Run database migrations and verify the resulting schema.
|
|
112
|
+
* @returns The verification result after migration.
|
|
113
|
+
*/
|
|
114
|
+
async install(options = {}) {
|
|
115
|
+
if (options.dryRun) {
|
|
116
|
+
return this.adapter.verify();
|
|
117
|
+
}
|
|
118
|
+
await this.adapter.initialize();
|
|
119
|
+
return this.adapter.verify();
|
|
120
|
+
}
|
|
121
|
+
/** Check that the schema is correct without running migrations. */
|
|
122
|
+
async verify() {
|
|
123
|
+
return this.adapter.verify();
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// src/services/CategoryService.ts
|
|
128
|
+
var CategoryService = class {
|
|
129
|
+
constructor(repo) {
|
|
130
|
+
this.repo = repo;
|
|
131
|
+
}
|
|
132
|
+
repo;
|
|
133
|
+
async create(input) {
|
|
134
|
+
return this.repo.create(input);
|
|
135
|
+
}
|
|
136
|
+
async get(id) {
|
|
137
|
+
return this.repo.get(id);
|
|
138
|
+
}
|
|
139
|
+
async update(id, input) {
|
|
140
|
+
return this.repo.update(id, input);
|
|
141
|
+
}
|
|
142
|
+
async delete(id) {
|
|
143
|
+
return this.repo.delete(id);
|
|
144
|
+
}
|
|
145
|
+
async list(filter) {
|
|
146
|
+
return this.repo.list(filter);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/services/ImageService.ts
|
|
151
|
+
var ImageService = class {
|
|
152
|
+
constructor(repo, storage) {
|
|
153
|
+
this.repo = repo;
|
|
154
|
+
this.storage = storage;
|
|
155
|
+
}
|
|
156
|
+
repo;
|
|
157
|
+
storage;
|
|
158
|
+
/**
|
|
159
|
+
* Upload a file via the storage adapter and create an image record.
|
|
160
|
+
* Requires a storage adapter to be configured.
|
|
161
|
+
*/
|
|
162
|
+
async upload(input) {
|
|
163
|
+
if (!this.storage) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"A storage adapter must be configured on OnlineCatalog to upload images."
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const url = await this.storage.upload(input.file, input.filename, {
|
|
169
|
+
contentType: input.contentType
|
|
170
|
+
});
|
|
171
|
+
return this.repo.create({
|
|
172
|
+
productId: input.productId,
|
|
173
|
+
url,
|
|
174
|
+
altText: input.altText,
|
|
175
|
+
sortOrder: input.sortOrder
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Associate an already-hosted image URL with a product. */
|
|
179
|
+
async addUrl(input) {
|
|
180
|
+
return this.repo.create(input);
|
|
181
|
+
}
|
|
182
|
+
async get(id) {
|
|
183
|
+
return this.repo.get(id);
|
|
184
|
+
}
|
|
185
|
+
async delete(id) {
|
|
186
|
+
const image2 = await this.repo.get(id);
|
|
187
|
+
if (!image2) return;
|
|
188
|
+
if (this.storage) {
|
|
189
|
+
await this.storage.delete(image2.url).catch(() => {
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
await this.repo.delete(id);
|
|
193
|
+
}
|
|
194
|
+
async listByProduct(productId) {
|
|
195
|
+
return this.repo.listByProduct(productId);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/services/ProductService.ts
|
|
200
|
+
var ProductService = class {
|
|
201
|
+
constructor(repo, storage) {
|
|
202
|
+
this.repo = repo;
|
|
203
|
+
this.storage = storage;
|
|
204
|
+
}
|
|
205
|
+
repo;
|
|
206
|
+
storage;
|
|
207
|
+
async create(input) {
|
|
208
|
+
return this.repo.create(input);
|
|
209
|
+
}
|
|
210
|
+
async get(id) {
|
|
211
|
+
return this.repo.get(id);
|
|
212
|
+
}
|
|
213
|
+
async update(id, input) {
|
|
214
|
+
return this.repo.update(id, input);
|
|
215
|
+
}
|
|
216
|
+
async delete(id) {
|
|
217
|
+
const product = await this.repo.get(id);
|
|
218
|
+
if (!product) return;
|
|
219
|
+
if (this.storage) {
|
|
220
|
+
for (const image2 of product.images) {
|
|
221
|
+
await this.storage.delete(image2.url).catch(() => {
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
await this.repo.delete(id);
|
|
226
|
+
}
|
|
227
|
+
async list(filter) {
|
|
228
|
+
return this.repo.list(filter);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/catalog.ts
|
|
233
|
+
var OnlineCatalog = class {
|
|
234
|
+
products;
|
|
235
|
+
categories;
|
|
236
|
+
images;
|
|
237
|
+
installer;
|
|
238
|
+
db;
|
|
239
|
+
constructor(config) {
|
|
240
|
+
this.db = config.db;
|
|
241
|
+
this.products = new ProductService(config.db.products, config.storage);
|
|
242
|
+
this.categories = new CategoryService(config.db.categories);
|
|
243
|
+
this.images = new ImageService(config.db.images, config.storage);
|
|
244
|
+
this.installer = new Installer(config.db);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Initialize the catalog. Runs database migrations on first use.
|
|
248
|
+
* Must be called before any other method.
|
|
249
|
+
*/
|
|
250
|
+
async initialize() {
|
|
251
|
+
await this.db.initialize();
|
|
252
|
+
}
|
|
253
|
+
/** Release all resources held by the database adapter. */
|
|
254
|
+
async close() {
|
|
255
|
+
await this.db.close();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/adapters/database/relational/BaseSQLAdapter.ts
|
|
260
|
+
var REQUIRED_TABLES = [
|
|
261
|
+
"occ_category",
|
|
262
|
+
"occ_product",
|
|
263
|
+
"occ_image",
|
|
264
|
+
"occ_migration"
|
|
265
|
+
];
|
|
266
|
+
var BaseSQLAdapter = class {
|
|
267
|
+
async initialize() {
|
|
268
|
+
const migration = this.migrationSql;
|
|
269
|
+
const statements = migration.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
270
|
+
for (const stmt of statements) {
|
|
271
|
+
await this.db.run(stmt);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async verify() {
|
|
275
|
+
const issues = [];
|
|
276
|
+
for (const table of REQUIRED_TABLES) {
|
|
277
|
+
const row = await this.db.get(
|
|
278
|
+
this.tableExistsQuery(table)
|
|
279
|
+
);
|
|
280
|
+
if (!row) {
|
|
281
|
+
issues.push(`Missing table: ${table}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { ok: issues.length === 0, issues };
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// src/utils/slug.ts
|
|
289
|
+
function generateSlug(value) {
|
|
290
|
+
return value.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/[\s_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// src/adapters/database/relational/rowMappers.ts
|
|
294
|
+
function toDate(value) {
|
|
295
|
+
return value instanceof Date ? value : new Date(value);
|
|
296
|
+
}
|
|
297
|
+
function productFromRow(row, images) {
|
|
298
|
+
return {
|
|
299
|
+
id: row.id,
|
|
300
|
+
name: row.name,
|
|
301
|
+
slug: row.slug,
|
|
302
|
+
description: JSON.parse(row.description),
|
|
303
|
+
price: row.price,
|
|
304
|
+
sku: row.sku,
|
|
305
|
+
categoryId: row.category_id,
|
|
306
|
+
images,
|
|
307
|
+
metadata: JSON.parse(row.metadata),
|
|
308
|
+
createdAt: toDate(row.created_at),
|
|
309
|
+
updatedAt: toDate(row.updated_at)
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function categoryFromRow(row) {
|
|
313
|
+
return {
|
|
314
|
+
id: row.id,
|
|
315
|
+
name: row.name,
|
|
316
|
+
slug: row.slug,
|
|
317
|
+
parentId: row.parent_id,
|
|
318
|
+
metadata: JSON.parse(row.metadata),
|
|
319
|
+
createdAt: toDate(row.created_at),
|
|
320
|
+
updatedAt: toDate(row.updated_at)
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
function imageFromRow(row) {
|
|
324
|
+
return {
|
|
325
|
+
id: row.id,
|
|
326
|
+
productId: row.product_id,
|
|
327
|
+
url: row.url,
|
|
328
|
+
altText: row.alt_text,
|
|
329
|
+
sortOrder: row.sort_order,
|
|
330
|
+
createdAt: toDate(row.created_at)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/adapters/database/relational/SQLCategoryRepository.ts
|
|
335
|
+
var SQLCategoryRepository = class {
|
|
336
|
+
constructor(db) {
|
|
337
|
+
this.db = db;
|
|
338
|
+
}
|
|
339
|
+
db;
|
|
340
|
+
async create(input) {
|
|
341
|
+
const id = randomUUID();
|
|
342
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
343
|
+
const slug = input.slug ?? generateSlug(input.name);
|
|
344
|
+
await this.db.run(
|
|
345
|
+
`INSERT INTO occ_category (id, name, slug, parent_id, metadata, created_at, updated_at)
|
|
346
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
347
|
+
[
|
|
348
|
+
id,
|
|
349
|
+
input.name,
|
|
350
|
+
slug,
|
|
351
|
+
input.parentId ?? null,
|
|
352
|
+
JSON.stringify(input.metadata ?? {}),
|
|
353
|
+
now,
|
|
354
|
+
now
|
|
355
|
+
]
|
|
356
|
+
);
|
|
357
|
+
const category = await this.get(id);
|
|
358
|
+
if (!category)
|
|
359
|
+
throw new Error(`Failed to fetch category after insert: ${id}`);
|
|
360
|
+
return category;
|
|
361
|
+
}
|
|
362
|
+
async get(id) {
|
|
363
|
+
const row = await this.db.get(
|
|
364
|
+
"SELECT * FROM occ_category WHERE id = ?",
|
|
365
|
+
[id]
|
|
366
|
+
);
|
|
367
|
+
return row ? categoryFromRow(row) : null;
|
|
368
|
+
}
|
|
369
|
+
async update(id, input) {
|
|
370
|
+
const existing = await this.get(id);
|
|
371
|
+
if (!existing) throw new Error(`Category not found: ${id}`);
|
|
372
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
373
|
+
await this.db.run(
|
|
374
|
+
`UPDATE occ_category SET
|
|
375
|
+
name = ?,
|
|
376
|
+
slug = ?,
|
|
377
|
+
parent_id = ?,
|
|
378
|
+
metadata = ?,
|
|
379
|
+
updated_at = ?
|
|
380
|
+
WHERE id = ?`,
|
|
381
|
+
[
|
|
382
|
+
input.name ?? existing.name,
|
|
383
|
+
input.slug ?? existing.slug,
|
|
384
|
+
input.parentId !== void 0 ? input.parentId : existing.parentId,
|
|
385
|
+
JSON.stringify(input.metadata ?? existing.metadata),
|
|
386
|
+
now,
|
|
387
|
+
id
|
|
388
|
+
]
|
|
389
|
+
);
|
|
390
|
+
const updated = await this.get(id);
|
|
391
|
+
if (!updated)
|
|
392
|
+
throw new Error(`Failed to fetch category after update: ${id}`);
|
|
393
|
+
return updated;
|
|
394
|
+
}
|
|
395
|
+
async delete(id) {
|
|
396
|
+
await this.db.run("DELETE FROM occ_category WHERE id = ?", [id]);
|
|
397
|
+
}
|
|
398
|
+
async list(filter = {}) {
|
|
399
|
+
const conditions = [];
|
|
400
|
+
const params = [];
|
|
401
|
+
if (filter.parentId !== void 0) {
|
|
402
|
+
if (filter.parentId === null) {
|
|
403
|
+
conditions.push("parent_id IS NULL");
|
|
404
|
+
} else {
|
|
405
|
+
conditions.push("parent_id = ?");
|
|
406
|
+
params.push(filter.parentId);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (filter.search) {
|
|
410
|
+
conditions.push("name LIKE ?");
|
|
411
|
+
params.push(`%${filter.search}%`);
|
|
412
|
+
}
|
|
413
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
414
|
+
const limit = filter.limit !== void 0 ? `LIMIT ${filter.limit}` : "";
|
|
415
|
+
const offset = filter.offset !== void 0 ? `OFFSET ${filter.offset}` : "";
|
|
416
|
+
const rows = await this.db.all(
|
|
417
|
+
`SELECT * FROM occ_category ${where} ORDER BY name ASC ${limit} ${offset}`,
|
|
418
|
+
params
|
|
419
|
+
);
|
|
420
|
+
return rows.map(categoryFromRow);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
var SQLImageRepository = class {
|
|
424
|
+
constructor(db) {
|
|
425
|
+
this.db = db;
|
|
426
|
+
}
|
|
427
|
+
db;
|
|
428
|
+
async create(input) {
|
|
429
|
+
const id = randomUUID();
|
|
430
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
431
|
+
const sortOrder = input.sortOrder ?? 0;
|
|
432
|
+
await this.db.run(
|
|
433
|
+
`INSERT INTO occ_image (id, product_id, url, alt_text, sort_order, created_at)
|
|
434
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
435
|
+
[id, input.productId, input.url, input.altText, sortOrder, now]
|
|
436
|
+
);
|
|
437
|
+
const image2 = await this.get(id);
|
|
438
|
+
if (!image2) throw new Error(`Failed to fetch image after insert: ${id}`);
|
|
439
|
+
return image2;
|
|
440
|
+
}
|
|
441
|
+
async get(id) {
|
|
442
|
+
const row = await this.db.get(
|
|
443
|
+
"SELECT * FROM occ_image WHERE id = ?",
|
|
444
|
+
[id]
|
|
445
|
+
);
|
|
446
|
+
return row ? imageFromRow(row) : null;
|
|
447
|
+
}
|
|
448
|
+
async delete(id) {
|
|
449
|
+
await this.db.run("DELETE FROM occ_image WHERE id = ?", [id]);
|
|
450
|
+
}
|
|
451
|
+
async listByProduct(productId) {
|
|
452
|
+
const rows = await this.db.all(
|
|
453
|
+
"SELECT * FROM occ_image WHERE product_id = ? ORDER BY sort_order ASC",
|
|
454
|
+
[productId]
|
|
455
|
+
);
|
|
456
|
+
return rows.map(imageFromRow);
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
var SQLProductRepository = class {
|
|
460
|
+
constructor(db) {
|
|
461
|
+
this.db = db;
|
|
462
|
+
}
|
|
463
|
+
db;
|
|
464
|
+
async create(input) {
|
|
465
|
+
const id = randomUUID();
|
|
466
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
467
|
+
const slug = input.slug ?? generateSlug(input.name);
|
|
468
|
+
await this.db.run(
|
|
469
|
+
`INSERT INTO occ_product
|
|
470
|
+
(id, name, slug, description, price, sku, category_id, metadata, created_at, updated_at)
|
|
471
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
472
|
+
[
|
|
473
|
+
id,
|
|
474
|
+
input.name,
|
|
475
|
+
slug,
|
|
476
|
+
JSON.stringify(input.description),
|
|
477
|
+
input.price,
|
|
478
|
+
input.sku ?? null,
|
|
479
|
+
input.categoryId ?? null,
|
|
480
|
+
JSON.stringify(input.metadata ?? {}),
|
|
481
|
+
now,
|
|
482
|
+
now
|
|
483
|
+
]
|
|
484
|
+
);
|
|
485
|
+
const product = await this.get(id);
|
|
486
|
+
if (!product)
|
|
487
|
+
throw new Error(`Failed to fetch product after insert: ${id}`);
|
|
488
|
+
return product;
|
|
489
|
+
}
|
|
490
|
+
async get(id) {
|
|
491
|
+
const row = await this.db.get(
|
|
492
|
+
"SELECT * FROM occ_product WHERE id = ?",
|
|
493
|
+
[id]
|
|
494
|
+
);
|
|
495
|
+
if (!row) return null;
|
|
496
|
+
const imageRows = await this.db.all(
|
|
497
|
+
"SELECT * FROM occ_image WHERE product_id = ? ORDER BY sort_order ASC",
|
|
498
|
+
[id]
|
|
499
|
+
);
|
|
500
|
+
return productFromRow(row, imageRows.map(imageFromRow));
|
|
501
|
+
}
|
|
502
|
+
async update(id, input) {
|
|
503
|
+
const existing = await this.get(id);
|
|
504
|
+
if (!existing) throw new Error(`Product not found: ${id}`);
|
|
505
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
506
|
+
await this.db.run(
|
|
507
|
+
`UPDATE occ_product SET
|
|
508
|
+
name = ?,
|
|
509
|
+
slug = ?,
|
|
510
|
+
description = ?,
|
|
511
|
+
price = ?,
|
|
512
|
+
sku = ?,
|
|
513
|
+
category_id = ?,
|
|
514
|
+
metadata = ?,
|
|
515
|
+
updated_at = ?
|
|
516
|
+
WHERE id = ?`,
|
|
517
|
+
[
|
|
518
|
+
input.name ?? existing.name,
|
|
519
|
+
input.slug ?? existing.slug,
|
|
520
|
+
JSON.stringify(input.description ?? existing.description),
|
|
521
|
+
input.price ?? existing.price,
|
|
522
|
+
input.sku !== void 0 ? input.sku : existing.sku,
|
|
523
|
+
input.categoryId !== void 0 ? input.categoryId : existing.categoryId,
|
|
524
|
+
JSON.stringify(input.metadata ?? existing.metadata),
|
|
525
|
+
now,
|
|
526
|
+
id
|
|
527
|
+
]
|
|
528
|
+
);
|
|
529
|
+
const updated = await this.get(id);
|
|
530
|
+
if (!updated)
|
|
531
|
+
throw new Error(`Failed to fetch product after update: ${id}`);
|
|
532
|
+
return updated;
|
|
533
|
+
}
|
|
534
|
+
async delete(id) {
|
|
535
|
+
await this.db.run("DELETE FROM occ_product WHERE id = ?", [id]);
|
|
536
|
+
}
|
|
537
|
+
async list(filter = {}) {
|
|
538
|
+
const conditions = [];
|
|
539
|
+
const params = [];
|
|
540
|
+
if (filter.categoryId !== void 0) {
|
|
541
|
+
conditions.push("category_id = ?");
|
|
542
|
+
params.push(filter.categoryId);
|
|
543
|
+
}
|
|
544
|
+
if (filter.search) {
|
|
545
|
+
conditions.push("(name LIKE ? OR sku LIKE ?)");
|
|
546
|
+
params.push(`%${filter.search}%`, `%${filter.search}%`);
|
|
547
|
+
}
|
|
548
|
+
if (filter.minPrice !== void 0) {
|
|
549
|
+
conditions.push("price >= ?");
|
|
550
|
+
params.push(filter.minPrice);
|
|
551
|
+
}
|
|
552
|
+
if (filter.maxPrice !== void 0) {
|
|
553
|
+
conditions.push("price <= ?");
|
|
554
|
+
params.push(filter.maxPrice);
|
|
555
|
+
}
|
|
556
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
557
|
+
const limit = filter.limit !== void 0 ? `LIMIT ${filter.limit}` : "";
|
|
558
|
+
const offset = filter.offset !== void 0 ? `OFFSET ${filter.offset}` : "";
|
|
559
|
+
const rows = await this.db.all(
|
|
560
|
+
`SELECT * FROM occ_product ${where} ORDER BY created_at DESC ${limit} ${offset}`,
|
|
561
|
+
params
|
|
562
|
+
);
|
|
563
|
+
const products = [];
|
|
564
|
+
for (const row of rows) {
|
|
565
|
+
const imageRows = await this.db.all(
|
|
566
|
+
"SELECT * FROM occ_image WHERE product_id = ? ORDER BY sort_order ASC",
|
|
567
|
+
[row.id]
|
|
568
|
+
);
|
|
569
|
+
products.push(productFromRow(row, imageRows.map(imageFromRow)));
|
|
570
|
+
}
|
|
571
|
+
return products;
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
// src/adapters/database/sqlite/SQLiteAdapter.ts
|
|
576
|
+
var BunSQLiteRunner = class {
|
|
577
|
+
// biome-ignore lint/suspicious/noExplicitAny: bun:sqlite Database type
|
|
578
|
+
constructor(db) {
|
|
579
|
+
this.db = db;
|
|
580
|
+
}
|
|
581
|
+
db;
|
|
582
|
+
async run(sql, params = []) {
|
|
583
|
+
this.db.run(sql, params);
|
|
584
|
+
}
|
|
585
|
+
async all(sql, params = []) {
|
|
586
|
+
return this.db.query(sql).all(...params);
|
|
587
|
+
}
|
|
588
|
+
async get(sql, params = []) {
|
|
589
|
+
return this.db.query(sql).get(...params);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
var BetterSqliteRunner = class {
|
|
593
|
+
// biome-ignore lint/suspicious/noExplicitAny: better-sqlite3 Database type
|
|
594
|
+
constructor(db) {
|
|
595
|
+
this.db = db;
|
|
596
|
+
}
|
|
597
|
+
db;
|
|
598
|
+
async run(sql, params = []) {
|
|
599
|
+
this.db.prepare(sql).run(...params);
|
|
600
|
+
}
|
|
601
|
+
async all(sql, params = []) {
|
|
602
|
+
return this.db.prepare(sql).all(...params);
|
|
603
|
+
}
|
|
604
|
+
async get(sql, params = []) {
|
|
605
|
+
return this.db.prepare(sql).get(...params);
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
async function createSQLiteInstance(filename) {
|
|
609
|
+
try {
|
|
610
|
+
const { Database } = await import('bun:sqlite');
|
|
611
|
+
const db = new Database(filename);
|
|
612
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
|
|
613
|
+
return {
|
|
614
|
+
runner: new BunSQLiteRunner(db),
|
|
615
|
+
instance: db,
|
|
616
|
+
close: () => db.close()
|
|
617
|
+
};
|
|
618
|
+
} catch {
|
|
619
|
+
}
|
|
620
|
+
try {
|
|
621
|
+
const { createRequire: createRequire6 } = await import('module');
|
|
622
|
+
const require2 = createRequire6(import.meta.url);
|
|
623
|
+
const Database = require2("better-sqlite3");
|
|
624
|
+
const db = new Database(filename);
|
|
625
|
+
db.pragma("journal_mode = WAL");
|
|
626
|
+
db.pragma("foreign_keys = ON");
|
|
627
|
+
return {
|
|
628
|
+
runner: new BetterSqliteRunner(db),
|
|
629
|
+
instance: db,
|
|
630
|
+
close: () => db.close()
|
|
631
|
+
};
|
|
632
|
+
} catch {
|
|
633
|
+
throw new Error(
|
|
634
|
+
"No SQLite driver found. In Bun environments, bun:sqlite is built-in. In Node.js environments, run: bun add better-sqlite3"
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
var __dirname$1 = fileURLToPath(new URL(".", import.meta.url));
|
|
639
|
+
var MIGRATION_PATH = join(
|
|
640
|
+
__dirname$1,
|
|
641
|
+
"../relational/migrations/001_initial.sql"
|
|
642
|
+
);
|
|
643
|
+
var SQLiteAdapter = class extends BaseSQLAdapter {
|
|
644
|
+
runner;
|
|
645
|
+
closeHandle;
|
|
646
|
+
config;
|
|
647
|
+
_products;
|
|
648
|
+
_categories;
|
|
649
|
+
_images;
|
|
650
|
+
constructor(config) {
|
|
651
|
+
super();
|
|
652
|
+
this.config = config;
|
|
653
|
+
}
|
|
654
|
+
get db() {
|
|
655
|
+
if (!this.runner) throw new Error("Call initialize() first");
|
|
656
|
+
return this.runner;
|
|
657
|
+
}
|
|
658
|
+
get products() {
|
|
659
|
+
if (!this._products) throw new Error("Call initialize() first");
|
|
660
|
+
return this._products;
|
|
661
|
+
}
|
|
662
|
+
get categories() {
|
|
663
|
+
if (!this._categories) throw new Error("Call initialize() first");
|
|
664
|
+
return this._categories;
|
|
665
|
+
}
|
|
666
|
+
get images() {
|
|
667
|
+
if (!this._images) throw new Error("Call initialize() first");
|
|
668
|
+
return this._images;
|
|
669
|
+
}
|
|
670
|
+
/** Must be called before any other method. Sets up the SQLite driver. */
|
|
671
|
+
async initialize() {
|
|
672
|
+
const { runner, close } = await createSQLiteInstance(this.config.filename);
|
|
673
|
+
this.runner = runner;
|
|
674
|
+
this._products = new SQLProductRepository(runner);
|
|
675
|
+
this._categories = new SQLCategoryRepository(runner);
|
|
676
|
+
this._images = new SQLImageRepository(runner);
|
|
677
|
+
this.closeHandle = close;
|
|
678
|
+
await super.initialize();
|
|
679
|
+
}
|
|
680
|
+
get migrationSql() {
|
|
681
|
+
return readFileSync(MIGRATION_PATH, "utf8");
|
|
682
|
+
}
|
|
683
|
+
tableExistsQuery(table) {
|
|
684
|
+
return `SELECT name FROM sqlite_master WHERE type='table' AND name='${table}'`;
|
|
685
|
+
}
|
|
686
|
+
async verify() {
|
|
687
|
+
return super.verify();
|
|
688
|
+
}
|
|
689
|
+
async close() {
|
|
690
|
+
this.closeHandle?.();
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
function loadDriver() {
|
|
694
|
+
try {
|
|
695
|
+
const require2 = createRequire(import.meta.url);
|
|
696
|
+
const mod = require2("postgres");
|
|
697
|
+
return mod.default ?? mod;
|
|
698
|
+
} catch {
|
|
699
|
+
throw new Error("postgres is not installed. Run: bun add postgres");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
var PostgresSQLRunner = class {
|
|
703
|
+
constructor(sql) {
|
|
704
|
+
this.sql = sql;
|
|
705
|
+
}
|
|
706
|
+
sql;
|
|
707
|
+
async run(query, params = []) {
|
|
708
|
+
await this.sql.unsafe(query, params);
|
|
709
|
+
}
|
|
710
|
+
async all(query, params = []) {
|
|
711
|
+
const rows = await this.sql.unsafe(query, params);
|
|
712
|
+
return rows;
|
|
713
|
+
}
|
|
714
|
+
async get(query, params = []) {
|
|
715
|
+
const rows = await this.sql.unsafe(query, params);
|
|
716
|
+
return rows[0];
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
720
|
+
var MIGRATION_PATH2 = join(
|
|
721
|
+
__dirname2,
|
|
722
|
+
"../relational/migrations/001_initial.sql"
|
|
723
|
+
);
|
|
724
|
+
var PostgresAdapter = class extends BaseSQLAdapter {
|
|
725
|
+
db;
|
|
726
|
+
products;
|
|
727
|
+
categories;
|
|
728
|
+
images;
|
|
729
|
+
sql;
|
|
730
|
+
constructor(config) {
|
|
731
|
+
super();
|
|
732
|
+
const connect = loadDriver();
|
|
733
|
+
this.sql = connect(config.url);
|
|
734
|
+
const runner = new PostgresSQLRunner(this.sql);
|
|
735
|
+
this.db = runner;
|
|
736
|
+
this.products = new SQLProductRepository(runner);
|
|
737
|
+
this.categories = new SQLCategoryRepository(runner);
|
|
738
|
+
this.images = new SQLImageRepository(runner);
|
|
739
|
+
}
|
|
740
|
+
get migrationSql() {
|
|
741
|
+
return readFileSync(MIGRATION_PATH2, "utf8");
|
|
742
|
+
}
|
|
743
|
+
tableExistsQuery(table) {
|
|
744
|
+
return `SELECT table_name AS name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = '${table}'`;
|
|
745
|
+
}
|
|
746
|
+
async verify() {
|
|
747
|
+
return super.verify();
|
|
748
|
+
}
|
|
749
|
+
async close() {
|
|
750
|
+
await this.sql.end();
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
function loadDriver2() {
|
|
754
|
+
try {
|
|
755
|
+
const require2 = createRequire(import.meta.url);
|
|
756
|
+
return require2("mysql2/promise");
|
|
757
|
+
} catch {
|
|
758
|
+
throw new Error("mysql2 is not installed. Run: bun add mysql2");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
var MySQLSQLRunner = class {
|
|
762
|
+
constructor(pool) {
|
|
763
|
+
this.pool = pool;
|
|
764
|
+
}
|
|
765
|
+
pool;
|
|
766
|
+
// mysql2 types require `ExecuteValues` (any[]), so we cast here.
|
|
767
|
+
async run(query, params = []) {
|
|
768
|
+
await this.pool.execute(query, params);
|
|
769
|
+
}
|
|
770
|
+
async all(query, params = []) {
|
|
771
|
+
const [rows] = await this.pool.execute(query, params);
|
|
772
|
+
return rows;
|
|
773
|
+
}
|
|
774
|
+
async get(query, params = []) {
|
|
775
|
+
const [rows] = await this.pool.execute(query, params);
|
|
776
|
+
return rows[0];
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
var __dirname3 = fileURLToPath(new URL(".", import.meta.url));
|
|
780
|
+
var MIGRATION_PATH3 = join(
|
|
781
|
+
__dirname3,
|
|
782
|
+
"../relational/migrations/001_initial.sql"
|
|
783
|
+
);
|
|
784
|
+
var MySQLAdapter = class extends BaseSQLAdapter {
|
|
785
|
+
db;
|
|
786
|
+
products;
|
|
787
|
+
categories;
|
|
788
|
+
images;
|
|
789
|
+
pool;
|
|
790
|
+
constructor(config) {
|
|
791
|
+
super();
|
|
792
|
+
const { createPool } = loadDriver2();
|
|
793
|
+
this.pool = createPool(config);
|
|
794
|
+
const runner = new MySQLSQLRunner(this.pool);
|
|
795
|
+
this.db = runner;
|
|
796
|
+
this.products = new SQLProductRepository(runner);
|
|
797
|
+
this.categories = new SQLCategoryRepository(runner);
|
|
798
|
+
this.images = new SQLImageRepository(runner);
|
|
799
|
+
}
|
|
800
|
+
get migrationSql() {
|
|
801
|
+
return readFileSync(MIGRATION_PATH3, "utf8").replace(
|
|
802
|
+
/ON CONFLICT \(version\) DO NOTHING/g,
|
|
803
|
+
""
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
tableExistsQuery(table) {
|
|
807
|
+
return `SELECT table_name AS name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = '${table}'`;
|
|
808
|
+
}
|
|
809
|
+
async verify() {
|
|
810
|
+
return super.verify();
|
|
811
|
+
}
|
|
812
|
+
async close() {
|
|
813
|
+
await this.pool.end();
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
function loadDriver3() {
|
|
817
|
+
try {
|
|
818
|
+
const require2 = createRequire(import.meta.url);
|
|
819
|
+
const mod = require2("ioredis");
|
|
820
|
+
return mod.default ?? mod;
|
|
821
|
+
} catch {
|
|
822
|
+
throw new Error("ioredis is not installed. Run: bun add ioredis");
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
var RedisAdapter = class {
|
|
826
|
+
client;
|
|
827
|
+
prefix;
|
|
828
|
+
products;
|
|
829
|
+
categories;
|
|
830
|
+
images;
|
|
831
|
+
constructor(config) {
|
|
832
|
+
const RedisClass = loadDriver3();
|
|
833
|
+
this.client = new RedisClass(config.url);
|
|
834
|
+
this.prefix = config.keyPrefix ?? "occ";
|
|
835
|
+
this.products = new RedisProductRepository(this.client, this.prefix);
|
|
836
|
+
this.categories = new RedisCategoryRepository(this.client, this.prefix);
|
|
837
|
+
this.images = new RedisImageRepository(this.client, this.prefix);
|
|
838
|
+
}
|
|
839
|
+
async initialize() {
|
|
840
|
+
await this.client.ping();
|
|
841
|
+
}
|
|
842
|
+
async verify() {
|
|
843
|
+
try {
|
|
844
|
+
await this.client.ping();
|
|
845
|
+
return { ok: true, issues: [] };
|
|
846
|
+
} catch (err) {
|
|
847
|
+
return {
|
|
848
|
+
ok: false,
|
|
849
|
+
issues: [`Redis connection failed: ${String(err)}`]
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
async close() {
|
|
854
|
+
await this.client.quit();
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
var RedisProductRepository = class {
|
|
858
|
+
constructor(client, prefix) {
|
|
859
|
+
this.client = client;
|
|
860
|
+
this.prefix = prefix;
|
|
861
|
+
}
|
|
862
|
+
client;
|
|
863
|
+
prefix;
|
|
864
|
+
key(id) {
|
|
865
|
+
return `${this.prefix}:product:${id}`;
|
|
866
|
+
}
|
|
867
|
+
async create(input) {
|
|
868
|
+
const id = randomUUID();
|
|
869
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
870
|
+
const product = {
|
|
871
|
+
id,
|
|
872
|
+
name: input.name,
|
|
873
|
+
slug: input.slug ?? generateSlug(input.name),
|
|
874
|
+
description: input.description,
|
|
875
|
+
price: input.price,
|
|
876
|
+
sku: input.sku ?? null,
|
|
877
|
+
categoryId: input.categoryId ?? null,
|
|
878
|
+
images: [],
|
|
879
|
+
metadata: input.metadata ?? {},
|
|
880
|
+
createdAt: new Date(now),
|
|
881
|
+
updatedAt: new Date(now)
|
|
882
|
+
};
|
|
883
|
+
await this.client.set(this.key(id), JSON.stringify(product));
|
|
884
|
+
await this.client.sadd(`${this.prefix}:products:all`, id);
|
|
885
|
+
if (product.categoryId) {
|
|
886
|
+
await this.client.sadd(
|
|
887
|
+
`${this.prefix}:products:cat:${product.categoryId}`,
|
|
888
|
+
id
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return product;
|
|
892
|
+
}
|
|
893
|
+
async get(id) {
|
|
894
|
+
const raw = await this.client.get(this.key(id));
|
|
895
|
+
if (!raw) return null;
|
|
896
|
+
const product = JSON.parse(raw);
|
|
897
|
+
product.createdAt = new Date(product.createdAt);
|
|
898
|
+
product.updatedAt = new Date(product.updatedAt);
|
|
899
|
+
return product;
|
|
900
|
+
}
|
|
901
|
+
async update(id, input) {
|
|
902
|
+
const existing = await this.get(id);
|
|
903
|
+
if (!existing) throw new Error(`Product not found: ${id}`);
|
|
904
|
+
const oldCategoryId = existing.categoryId;
|
|
905
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
906
|
+
const updated = {
|
|
907
|
+
...existing,
|
|
908
|
+
name: input.name ?? existing.name,
|
|
909
|
+
slug: input.slug ?? existing.slug,
|
|
910
|
+
description: input.description ?? existing.description,
|
|
911
|
+
price: input.price ?? existing.price,
|
|
912
|
+
sku: input.sku !== void 0 ? input.sku : existing.sku,
|
|
913
|
+
categoryId: input.categoryId !== void 0 ? input.categoryId : existing.categoryId,
|
|
914
|
+
metadata: input.metadata ?? existing.metadata,
|
|
915
|
+
updatedAt: new Date(now)
|
|
916
|
+
};
|
|
917
|
+
await this.client.set(this.key(id), JSON.stringify(updated));
|
|
918
|
+
if (oldCategoryId !== updated.categoryId) {
|
|
919
|
+
if (oldCategoryId) {
|
|
920
|
+
await this.client.srem(
|
|
921
|
+
`${this.prefix}:products:cat:${oldCategoryId}`,
|
|
922
|
+
id
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
if (updated.categoryId) {
|
|
926
|
+
await this.client.sadd(
|
|
927
|
+
`${this.prefix}:products:cat:${updated.categoryId}`,
|
|
928
|
+
id
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return updated;
|
|
933
|
+
}
|
|
934
|
+
async delete(id) {
|
|
935
|
+
const existing = await this.get(id);
|
|
936
|
+
await this.client.del(this.key(id));
|
|
937
|
+
await this.client.srem(`${this.prefix}:products:all`, id);
|
|
938
|
+
if (existing?.categoryId) {
|
|
939
|
+
await this.client.srem(
|
|
940
|
+
`${this.prefix}:products:cat:${existing.categoryId}`,
|
|
941
|
+
id
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
async list(filter = {}) {
|
|
946
|
+
let ids;
|
|
947
|
+
if (filter.categoryId !== void 0) {
|
|
948
|
+
ids = await this.client.smembers(
|
|
949
|
+
`${this.prefix}:products:cat:${filter.categoryId}`
|
|
950
|
+
);
|
|
951
|
+
} else {
|
|
952
|
+
ids = await this.client.smembers(`${this.prefix}:products:all`);
|
|
953
|
+
}
|
|
954
|
+
const products = [];
|
|
955
|
+
for (const id of ids) {
|
|
956
|
+
const product = await this.get(id);
|
|
957
|
+
if (!product) continue;
|
|
958
|
+
if (filter.search && !product.name.toLowerCase().includes(filter.search.toLowerCase()))
|
|
959
|
+
continue;
|
|
960
|
+
if (filter.minPrice !== void 0 && product.price < filter.minPrice)
|
|
961
|
+
continue;
|
|
962
|
+
if (filter.maxPrice !== void 0 && product.price > filter.maxPrice)
|
|
963
|
+
continue;
|
|
964
|
+
products.push(product);
|
|
965
|
+
}
|
|
966
|
+
products.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
967
|
+
const start = filter.offset ?? 0;
|
|
968
|
+
const end = filter.limit !== void 0 ? start + filter.limit : void 0;
|
|
969
|
+
return products.slice(start, end);
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
var RedisCategoryRepository = class {
|
|
973
|
+
constructor(client, prefix) {
|
|
974
|
+
this.client = client;
|
|
975
|
+
this.prefix = prefix;
|
|
976
|
+
}
|
|
977
|
+
client;
|
|
978
|
+
prefix;
|
|
979
|
+
key(id) {
|
|
980
|
+
return `${this.prefix}:category:${id}`;
|
|
981
|
+
}
|
|
982
|
+
async create(input) {
|
|
983
|
+
const id = randomUUID();
|
|
984
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
985
|
+
const category = {
|
|
986
|
+
id,
|
|
987
|
+
name: input.name,
|
|
988
|
+
slug: input.slug ?? generateSlug(input.name),
|
|
989
|
+
parentId: input.parentId ?? null,
|
|
990
|
+
metadata: input.metadata ?? {},
|
|
991
|
+
createdAt: new Date(now),
|
|
992
|
+
updatedAt: new Date(now)
|
|
993
|
+
};
|
|
994
|
+
await this.client.set(this.key(id), JSON.stringify(category));
|
|
995
|
+
await this.client.sadd(`${this.prefix}:categories:all`, id);
|
|
996
|
+
return category;
|
|
997
|
+
}
|
|
998
|
+
async get(id) {
|
|
999
|
+
const raw = await this.client.get(this.key(id));
|
|
1000
|
+
if (!raw) return null;
|
|
1001
|
+
const category = JSON.parse(raw);
|
|
1002
|
+
category.createdAt = new Date(category.createdAt);
|
|
1003
|
+
category.updatedAt = new Date(category.updatedAt);
|
|
1004
|
+
return category;
|
|
1005
|
+
}
|
|
1006
|
+
async update(id, input) {
|
|
1007
|
+
const existing = await this.get(id);
|
|
1008
|
+
if (!existing) throw new Error(`Category not found: ${id}`);
|
|
1009
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1010
|
+
const updated = {
|
|
1011
|
+
...existing,
|
|
1012
|
+
name: input.name ?? existing.name,
|
|
1013
|
+
slug: input.slug ?? existing.slug,
|
|
1014
|
+
parentId: input.parentId !== void 0 ? input.parentId : existing.parentId,
|
|
1015
|
+
metadata: input.metadata ?? existing.metadata,
|
|
1016
|
+
updatedAt: new Date(now)
|
|
1017
|
+
};
|
|
1018
|
+
await this.client.set(this.key(id), JSON.stringify(updated));
|
|
1019
|
+
return updated;
|
|
1020
|
+
}
|
|
1021
|
+
async delete(id) {
|
|
1022
|
+
await this.client.del(this.key(id));
|
|
1023
|
+
await this.client.srem(`${this.prefix}:categories:all`, id);
|
|
1024
|
+
}
|
|
1025
|
+
async list(filter = {}) {
|
|
1026
|
+
const ids = await this.client.smembers(`${this.prefix}:categories:all`);
|
|
1027
|
+
const categories = [];
|
|
1028
|
+
for (const id of ids) {
|
|
1029
|
+
const category = await this.get(id);
|
|
1030
|
+
if (!category) continue;
|
|
1031
|
+
if (filter.parentId !== void 0) {
|
|
1032
|
+
if (category.parentId !== filter.parentId) continue;
|
|
1033
|
+
}
|
|
1034
|
+
if (filter.search && !category.name.toLowerCase().includes(filter.search.toLowerCase()))
|
|
1035
|
+
continue;
|
|
1036
|
+
categories.push(category);
|
|
1037
|
+
}
|
|
1038
|
+
categories.sort((a, b) => a.name.localeCompare(b.name));
|
|
1039
|
+
const start = filter.offset ?? 0;
|
|
1040
|
+
const end = filter.limit !== void 0 ? start + filter.limit : void 0;
|
|
1041
|
+
return categories.slice(start, end);
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
var RedisImageRepository = class {
|
|
1045
|
+
constructor(client, prefix) {
|
|
1046
|
+
this.client = client;
|
|
1047
|
+
this.prefix = prefix;
|
|
1048
|
+
}
|
|
1049
|
+
client;
|
|
1050
|
+
prefix;
|
|
1051
|
+
key(id) {
|
|
1052
|
+
return `${this.prefix}:image:${id}`;
|
|
1053
|
+
}
|
|
1054
|
+
async create(input) {
|
|
1055
|
+
const id = randomUUID();
|
|
1056
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1057
|
+
const image2 = {
|
|
1058
|
+
id,
|
|
1059
|
+
productId: input.productId,
|
|
1060
|
+
url: input.url,
|
|
1061
|
+
altText: input.altText,
|
|
1062
|
+
sortOrder: input.sortOrder ?? 0,
|
|
1063
|
+
createdAt: new Date(now)
|
|
1064
|
+
};
|
|
1065
|
+
await this.client.set(this.key(id), JSON.stringify(image2));
|
|
1066
|
+
await this.client.zadd(
|
|
1067
|
+
`${this.prefix}:images:product:${input.productId}`,
|
|
1068
|
+
image2.sortOrder,
|
|
1069
|
+
id
|
|
1070
|
+
);
|
|
1071
|
+
return image2;
|
|
1072
|
+
}
|
|
1073
|
+
async get(id) {
|
|
1074
|
+
const raw = await this.client.get(this.key(id));
|
|
1075
|
+
if (!raw) return null;
|
|
1076
|
+
const image2 = JSON.parse(raw);
|
|
1077
|
+
image2.createdAt = new Date(image2.createdAt);
|
|
1078
|
+
return image2;
|
|
1079
|
+
}
|
|
1080
|
+
async delete(id) {
|
|
1081
|
+
const image2 = await this.get(id);
|
|
1082
|
+
await this.client.del(this.key(id));
|
|
1083
|
+
if (image2) {
|
|
1084
|
+
await this.client.zrem(
|
|
1085
|
+
`${this.prefix}:images:product:${image2.productId}`,
|
|
1086
|
+
id
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async listByProduct(productId) {
|
|
1091
|
+
const ids = await this.client.zrange(
|
|
1092
|
+
`${this.prefix}:images:product:${productId}`,
|
|
1093
|
+
0,
|
|
1094
|
+
-1
|
|
1095
|
+
);
|
|
1096
|
+
const images = [];
|
|
1097
|
+
for (const id of ids) {
|
|
1098
|
+
const image2 = await this.get(id);
|
|
1099
|
+
if (image2) images.push(image2);
|
|
1100
|
+
}
|
|
1101
|
+
return images;
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
function loadDriver4() {
|
|
1105
|
+
try {
|
|
1106
|
+
const require2 = createRequire(import.meta.url);
|
|
1107
|
+
const { MongoClient } = require2("mongodb");
|
|
1108
|
+
return MongoClient;
|
|
1109
|
+
} catch {
|
|
1110
|
+
throw new Error("mongodb is not installed. Run: bun add mongodb");
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
var MongoDBAdapter = class {
|
|
1114
|
+
client;
|
|
1115
|
+
dbName;
|
|
1116
|
+
products;
|
|
1117
|
+
categories;
|
|
1118
|
+
images;
|
|
1119
|
+
constructor(config) {
|
|
1120
|
+
const MongoClient = loadDriver4();
|
|
1121
|
+
this.client = new MongoClient(config.url);
|
|
1122
|
+
this.dbName = config.database ?? "online_catalog";
|
|
1123
|
+
const db = () => this.client.db(this.dbName);
|
|
1124
|
+
this.products = new MongoProductRepository(db);
|
|
1125
|
+
this.categories = new MongoCategoryRepository(db);
|
|
1126
|
+
this.images = new MongoImageRepository(db);
|
|
1127
|
+
}
|
|
1128
|
+
async initialize() {
|
|
1129
|
+
await this.client.connect();
|
|
1130
|
+
const db = this.client.db(this.dbName);
|
|
1131
|
+
await db.collection("occ_product").createIndex({ slug: 1 }, { unique: true });
|
|
1132
|
+
await db.collection("occ_product").createIndex({ categoryId: 1 });
|
|
1133
|
+
await db.collection("occ_category").createIndex({ slug: 1 }, { unique: true });
|
|
1134
|
+
await db.collection("occ_image").createIndex({ productId: 1, sortOrder: 1 });
|
|
1135
|
+
}
|
|
1136
|
+
async verify() {
|
|
1137
|
+
try {
|
|
1138
|
+
await this.client.db(this.dbName).command({ ping: 1 });
|
|
1139
|
+
return { ok: true, issues: [] };
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
return {
|
|
1142
|
+
ok: false,
|
|
1143
|
+
issues: [`MongoDB connection failed: ${String(err)}`]
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async close() {
|
|
1148
|
+
await this.client.close();
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
var MongoProductRepository = class {
|
|
1152
|
+
constructor(db) {
|
|
1153
|
+
this.db = db;
|
|
1154
|
+
}
|
|
1155
|
+
db;
|
|
1156
|
+
col() {
|
|
1157
|
+
return this.db().collection("occ_product");
|
|
1158
|
+
}
|
|
1159
|
+
imgCol() {
|
|
1160
|
+
return this.db().collection("occ_image");
|
|
1161
|
+
}
|
|
1162
|
+
async toProduct(doc) {
|
|
1163
|
+
const images = await this.imgCol().find({ productId: doc._id }).sort({ sortOrder: 1 }).toArray();
|
|
1164
|
+
return {
|
|
1165
|
+
id: doc._id,
|
|
1166
|
+
name: doc.name,
|
|
1167
|
+
slug: doc.slug,
|
|
1168
|
+
description: doc.description,
|
|
1169
|
+
price: doc.price,
|
|
1170
|
+
sku: doc.sku,
|
|
1171
|
+
categoryId: doc.categoryId,
|
|
1172
|
+
images: images.map((img) => ({
|
|
1173
|
+
id: img._id,
|
|
1174
|
+
productId: img.productId,
|
|
1175
|
+
url: img.url,
|
|
1176
|
+
altText: img.altText,
|
|
1177
|
+
sortOrder: img.sortOrder,
|
|
1178
|
+
createdAt: img.createdAt
|
|
1179
|
+
})),
|
|
1180
|
+
metadata: doc.metadata,
|
|
1181
|
+
createdAt: doc.createdAt,
|
|
1182
|
+
updatedAt: doc.updatedAt
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
async create(input) {
|
|
1186
|
+
const id = randomUUID();
|
|
1187
|
+
const now = /* @__PURE__ */ new Date();
|
|
1188
|
+
const doc = {
|
|
1189
|
+
_id: id,
|
|
1190
|
+
name: input.name,
|
|
1191
|
+
slug: input.slug ?? generateSlug(input.name),
|
|
1192
|
+
description: input.description,
|
|
1193
|
+
price: input.price,
|
|
1194
|
+
sku: input.sku ?? null,
|
|
1195
|
+
categoryId: input.categoryId ?? null,
|
|
1196
|
+
metadata: input.metadata ?? {},
|
|
1197
|
+
createdAt: now,
|
|
1198
|
+
updatedAt: now
|
|
1199
|
+
};
|
|
1200
|
+
await this.col().insertOne(doc);
|
|
1201
|
+
return this.toProduct(doc);
|
|
1202
|
+
}
|
|
1203
|
+
async get(id) {
|
|
1204
|
+
const doc = await this.col().findOne({ _id: id });
|
|
1205
|
+
if (!doc) return null;
|
|
1206
|
+
return this.toProduct(doc);
|
|
1207
|
+
}
|
|
1208
|
+
async update(id, input) {
|
|
1209
|
+
const existing = await this.get(id);
|
|
1210
|
+
if (!existing) throw new Error(`Product not found: ${id}`);
|
|
1211
|
+
const now = /* @__PURE__ */ new Date();
|
|
1212
|
+
await this.col().updateOne(
|
|
1213
|
+
{ _id: id },
|
|
1214
|
+
{
|
|
1215
|
+
$set: {
|
|
1216
|
+
name: input.name ?? existing.name,
|
|
1217
|
+
slug: input.slug ?? existing.slug,
|
|
1218
|
+
description: input.description ?? existing.description,
|
|
1219
|
+
price: input.price ?? existing.price,
|
|
1220
|
+
sku: input.sku !== void 0 ? input.sku : existing.sku,
|
|
1221
|
+
categoryId: input.categoryId !== void 0 ? input.categoryId : existing.categoryId,
|
|
1222
|
+
metadata: input.metadata ?? existing.metadata,
|
|
1223
|
+
updatedAt: now
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
const updated = await this.get(id);
|
|
1228
|
+
if (!updated)
|
|
1229
|
+
throw new Error(`Failed to fetch product after update: ${id}`);
|
|
1230
|
+
return updated;
|
|
1231
|
+
}
|
|
1232
|
+
async delete(id) {
|
|
1233
|
+
await this.col().deleteOne({ _id: id });
|
|
1234
|
+
}
|
|
1235
|
+
async list(filter = {}) {
|
|
1236
|
+
const query = {};
|
|
1237
|
+
if (filter.categoryId !== void 0) query.categoryId = filter.categoryId;
|
|
1238
|
+
if (filter.minPrice !== void 0 || filter.maxPrice !== void 0) {
|
|
1239
|
+
query.price = {};
|
|
1240
|
+
if (filter.minPrice !== void 0) query.price.$gte = filter.minPrice;
|
|
1241
|
+
if (filter.maxPrice !== void 0) query.price.$lte = filter.maxPrice;
|
|
1242
|
+
}
|
|
1243
|
+
if (filter.search) {
|
|
1244
|
+
query.$or = [
|
|
1245
|
+
{ name: { $regex: filter.search, $options: "i" } },
|
|
1246
|
+
{ sku: { $regex: filter.search, $options: "i" } }
|
|
1247
|
+
];
|
|
1248
|
+
}
|
|
1249
|
+
let cursor = this.col().find(query).sort({ createdAt: -1 });
|
|
1250
|
+
if (filter.offset) cursor = cursor.skip(filter.offset);
|
|
1251
|
+
if (filter.limit) cursor = cursor.limit(filter.limit);
|
|
1252
|
+
const docs = await cursor.toArray();
|
|
1253
|
+
return Promise.all(docs.map((doc) => this.toProduct(doc)));
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
var MongoCategoryRepository = class {
|
|
1257
|
+
constructor(db) {
|
|
1258
|
+
this.db = db;
|
|
1259
|
+
}
|
|
1260
|
+
db;
|
|
1261
|
+
col() {
|
|
1262
|
+
return this.db().collection("occ_category");
|
|
1263
|
+
}
|
|
1264
|
+
toCategory(doc) {
|
|
1265
|
+
return {
|
|
1266
|
+
id: doc._id,
|
|
1267
|
+
name: doc.name,
|
|
1268
|
+
slug: doc.slug,
|
|
1269
|
+
parentId: doc.parentId,
|
|
1270
|
+
metadata: doc.metadata,
|
|
1271
|
+
createdAt: doc.createdAt,
|
|
1272
|
+
updatedAt: doc.updatedAt
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
async create(input) {
|
|
1276
|
+
const id = randomUUID();
|
|
1277
|
+
const now = /* @__PURE__ */ new Date();
|
|
1278
|
+
const doc = {
|
|
1279
|
+
_id: id,
|
|
1280
|
+
name: input.name,
|
|
1281
|
+
slug: input.slug ?? generateSlug(input.name),
|
|
1282
|
+
parentId: input.parentId ?? null,
|
|
1283
|
+
metadata: input.metadata ?? {},
|
|
1284
|
+
createdAt: now,
|
|
1285
|
+
updatedAt: now
|
|
1286
|
+
};
|
|
1287
|
+
await this.col().insertOne(doc);
|
|
1288
|
+
return this.toCategory(doc);
|
|
1289
|
+
}
|
|
1290
|
+
async get(id) {
|
|
1291
|
+
const doc = await this.col().findOne({ _id: id });
|
|
1292
|
+
return doc ? this.toCategory(doc) : null;
|
|
1293
|
+
}
|
|
1294
|
+
async update(id, input) {
|
|
1295
|
+
const existing = await this.get(id);
|
|
1296
|
+
if (!existing) throw new Error(`Category not found: ${id}`);
|
|
1297
|
+
const now = /* @__PURE__ */ new Date();
|
|
1298
|
+
await this.col().updateOne(
|
|
1299
|
+
{ _id: id },
|
|
1300
|
+
{
|
|
1301
|
+
$set: {
|
|
1302
|
+
name: input.name ?? existing.name,
|
|
1303
|
+
slug: input.slug ?? existing.slug,
|
|
1304
|
+
parentId: input.parentId !== void 0 ? input.parentId : existing.parentId,
|
|
1305
|
+
metadata: input.metadata ?? existing.metadata,
|
|
1306
|
+
updatedAt: now
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
const updated = await this.get(id);
|
|
1311
|
+
if (!updated)
|
|
1312
|
+
throw new Error(`Failed to fetch category after update: ${id}`);
|
|
1313
|
+
return updated;
|
|
1314
|
+
}
|
|
1315
|
+
async delete(id) {
|
|
1316
|
+
await this.col().deleteOne({ _id: id });
|
|
1317
|
+
}
|
|
1318
|
+
async list(filter = {}) {
|
|
1319
|
+
const query = {};
|
|
1320
|
+
if (filter.parentId !== void 0) query.parentId = filter.parentId;
|
|
1321
|
+
if (filter.search) {
|
|
1322
|
+
query.name = { $regex: filter.search, $options: "i" };
|
|
1323
|
+
}
|
|
1324
|
+
let cursor = this.col().find(query).sort({ name: 1 });
|
|
1325
|
+
if (filter.offset) cursor = cursor.skip(filter.offset);
|
|
1326
|
+
if (filter.limit) cursor = cursor.limit(filter.limit);
|
|
1327
|
+
const docs = await cursor.toArray();
|
|
1328
|
+
return docs.map((doc) => this.toCategory(doc));
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
var MongoImageRepository = class {
|
|
1332
|
+
constructor(db) {
|
|
1333
|
+
this.db = db;
|
|
1334
|
+
}
|
|
1335
|
+
db;
|
|
1336
|
+
col() {
|
|
1337
|
+
return this.db().collection("occ_image");
|
|
1338
|
+
}
|
|
1339
|
+
async create(input) {
|
|
1340
|
+
const id = randomUUID();
|
|
1341
|
+
const now = /* @__PURE__ */ new Date();
|
|
1342
|
+
const doc = {
|
|
1343
|
+
_id: id,
|
|
1344
|
+
productId: input.productId,
|
|
1345
|
+
url: input.url,
|
|
1346
|
+
altText: input.altText,
|
|
1347
|
+
sortOrder: input.sortOrder ?? 0,
|
|
1348
|
+
createdAt: now
|
|
1349
|
+
};
|
|
1350
|
+
await this.col().insertOne(doc);
|
|
1351
|
+
return {
|
|
1352
|
+
id: doc._id,
|
|
1353
|
+
productId: doc.productId,
|
|
1354
|
+
url: doc.url,
|
|
1355
|
+
altText: doc.altText,
|
|
1356
|
+
sortOrder: doc.sortOrder,
|
|
1357
|
+
createdAt: doc.createdAt
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
async get(id) {
|
|
1361
|
+
const doc = await this.col().findOne({ _id: id });
|
|
1362
|
+
if (!doc) return null;
|
|
1363
|
+
return {
|
|
1364
|
+
id: doc._id,
|
|
1365
|
+
productId: doc.productId,
|
|
1366
|
+
url: doc.url,
|
|
1367
|
+
altText: doc.altText,
|
|
1368
|
+
sortOrder: doc.sortOrder,
|
|
1369
|
+
createdAt: doc.createdAt
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
async delete(id) {
|
|
1373
|
+
await this.col().deleteOne({ _id: id });
|
|
1374
|
+
}
|
|
1375
|
+
async listByProduct(productId) {
|
|
1376
|
+
const docs = await this.col().find({ productId }).sort({ sortOrder: 1 }).toArray();
|
|
1377
|
+
return docs.map((doc) => ({
|
|
1378
|
+
id: doc._id,
|
|
1379
|
+
productId: doc.productId,
|
|
1380
|
+
url: doc.url,
|
|
1381
|
+
altText: doc.altText,
|
|
1382
|
+
sortOrder: doc.sortOrder,
|
|
1383
|
+
createdAt: doc.createdAt
|
|
1384
|
+
}));
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
var LocalStorageAdapter = class {
|
|
1388
|
+
uploadDir;
|
|
1389
|
+
baseUrl;
|
|
1390
|
+
constructor(config) {
|
|
1391
|
+
this.uploadDir = resolve(config.uploadDir);
|
|
1392
|
+
this.baseUrl = config.baseUrl ?? "/uploads";
|
|
1393
|
+
if (!existsSync(this.uploadDir)) {
|
|
1394
|
+
mkdirSync(this.uploadDir, { recursive: true });
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
async upload(file, filename, _options) {
|
|
1398
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1399
|
+
const uniqueName = `${Date.now()}-${safeName}`;
|
|
1400
|
+
const filePath = join(this.uploadDir, uniqueName);
|
|
1401
|
+
if (Buffer.isBuffer(file)) {
|
|
1402
|
+
await Bun.write(filePath, file);
|
|
1403
|
+
} else {
|
|
1404
|
+
const writeStream = createWriteStream(filePath);
|
|
1405
|
+
await pipeline(file, writeStream);
|
|
1406
|
+
}
|
|
1407
|
+
return this.getPublicUrl(uniqueName);
|
|
1408
|
+
}
|
|
1409
|
+
async delete(url) {
|
|
1410
|
+
const key = this.urlToKey(url);
|
|
1411
|
+
if (!key) return;
|
|
1412
|
+
const filePath = join(this.uploadDir, key);
|
|
1413
|
+
if (existsSync(filePath)) {
|
|
1414
|
+
unlinkSync(filePath);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
getPublicUrl(key) {
|
|
1418
|
+
return `${this.baseUrl}/${key}`;
|
|
1419
|
+
}
|
|
1420
|
+
urlToKey(url) {
|
|
1421
|
+
const prefix = `${this.baseUrl}/`;
|
|
1422
|
+
if (url.startsWith(prefix)) {
|
|
1423
|
+
return url.slice(prefix.length);
|
|
1424
|
+
}
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
function loadDriver5() {
|
|
1429
|
+
try {
|
|
1430
|
+
const require2 = createRequire(import.meta.url);
|
|
1431
|
+
return require2("@aws-sdk/client-s3");
|
|
1432
|
+
} catch {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
"@aws-sdk/client-s3 is not installed. Run: bun add @aws-sdk/client-s3"
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
var S3Adapter = class {
|
|
1439
|
+
client;
|
|
1440
|
+
bucket;
|
|
1441
|
+
publicRead;
|
|
1442
|
+
baseUrl;
|
|
1443
|
+
aws;
|
|
1444
|
+
constructor(config) {
|
|
1445
|
+
this.aws = loadDriver5();
|
|
1446
|
+
this.bucket = config.bucket;
|
|
1447
|
+
this.publicRead = config.publicRead ?? true;
|
|
1448
|
+
this.client = new this.aws.S3Client({
|
|
1449
|
+
region: config.region,
|
|
1450
|
+
endpoint: config.endpoint,
|
|
1451
|
+
credentials: {
|
|
1452
|
+
accessKeyId: config.accessKeyId,
|
|
1453
|
+
secretAccessKey: config.secretAccessKey
|
|
1454
|
+
},
|
|
1455
|
+
forcePathStyle: !!config.endpoint
|
|
1456
|
+
});
|
|
1457
|
+
if (config.endpoint) {
|
|
1458
|
+
this.baseUrl = `${config.endpoint.replace(/\/$/, "")}/${config.bucket}`;
|
|
1459
|
+
} else {
|
|
1460
|
+
this.baseUrl = `https://${config.bucket}.s3.${config.region}.amazonaws.com`;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
async upload(file, filename, options) {
|
|
1464
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._/-]/g, "_");
|
|
1465
|
+
const key = `${Date.now()}-${safeName}`;
|
|
1466
|
+
const contentType = options?.contentType ?? guessContentType(filename);
|
|
1467
|
+
const command = new this.aws.PutObjectCommand({
|
|
1468
|
+
Bucket: this.bucket,
|
|
1469
|
+
Key: key,
|
|
1470
|
+
Body: file,
|
|
1471
|
+
ContentType: contentType,
|
|
1472
|
+
ACL: this.publicRead ? "public-read" : "private"
|
|
1473
|
+
});
|
|
1474
|
+
await this.client.send(command);
|
|
1475
|
+
return this.getPublicUrl(key);
|
|
1476
|
+
}
|
|
1477
|
+
async delete(url) {
|
|
1478
|
+
const key = this.urlToKey(url);
|
|
1479
|
+
if (!key) return;
|
|
1480
|
+
const command = new this.aws.DeleteObjectCommand({
|
|
1481
|
+
Bucket: this.bucket,
|
|
1482
|
+
Key: key
|
|
1483
|
+
});
|
|
1484
|
+
await this.client.send(command);
|
|
1485
|
+
}
|
|
1486
|
+
getPublicUrl(key) {
|
|
1487
|
+
return `${this.baseUrl}/${key}`;
|
|
1488
|
+
}
|
|
1489
|
+
urlToKey(url) {
|
|
1490
|
+
const prefix = `${this.baseUrl}/`;
|
|
1491
|
+
if (url.startsWith(prefix)) {
|
|
1492
|
+
return url.slice(prefix.length);
|
|
1493
|
+
}
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1497
|
+
function guessContentType(filename) {
|
|
1498
|
+
const ext = extname(filename).toLowerCase();
|
|
1499
|
+
const map = {
|
|
1500
|
+
".jpg": "image/jpeg",
|
|
1501
|
+
".jpeg": "image/jpeg",
|
|
1502
|
+
".png": "image/png",
|
|
1503
|
+
".gif": "image/gif",
|
|
1504
|
+
".webp": "image/webp",
|
|
1505
|
+
".svg": "image/svg+xml",
|
|
1506
|
+
".avif": "image/avif"
|
|
1507
|
+
};
|
|
1508
|
+
return map[ext] ?? "application/octet-stream";
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/adapters/storage/external/ExternalURLAdapter.ts
|
|
1512
|
+
var ExternalURLAdapter = class {
|
|
1513
|
+
async upload(_file, _filename, _options) {
|
|
1514
|
+
throw new Error(
|
|
1515
|
+
"ExternalURLAdapter does not support uploads. Pass the image URL directly in CreateImageInput.url."
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
async delete(_url) {
|
|
1519
|
+
}
|
|
1520
|
+
getPublicUrl(key) {
|
|
1521
|
+
return key;
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
export { ExternalURLAdapter, Installer, LocalStorageAdapter, MongoDBAdapter, MySQLAdapter, OnlineCatalog, PostgresAdapter, RedisAdapter, S3Adapter, SQLiteAdapter, assertRichTextDocument, blockquote, document, heading, image, isRichTextDocument, link, orderedList, paragraph, text, unorderedList };
|
|
1526
|
+
//# sourceMappingURL=index.js.map
|
|
1527
|
+
//# sourceMappingURL=index.js.map
|