@gravito/satellite-catalog 0.1.1

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/.dockerignore ADDED
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ dist
3
+ .git
4
+ .env
5
+ *.log
6
+ .vscode
7
+ .idea
8
+ tests
package/.env.example ADDED
@@ -0,0 +1,19 @@
1
+ # Application
2
+ APP_NAME="catalog"
3
+ APP_ENV=development
4
+ APP_KEY=
5
+ APP_DEBUG=true
6
+ APP_URL=http://localhost:3000
7
+
8
+ # Server
9
+ PORT=3000
10
+
11
+ # Database
12
+ DB_CONNECTION=sqlite
13
+ DB_DATABASE=database/database.sqlite
14
+
15
+ # Cache
16
+ CACHE_DRIVER=memory
17
+
18
+ # Logging
19
+ LOG_LEVEL=debug
@@ -0,0 +1,14 @@
1
+ # catalog Satellite Architecture
2
+
3
+ This satellite follows the Gravito Satellite Specification v1.0.
4
+
5
+ ## Design
6
+ - **DDD**: Domain logic is separated from framework concerns.
7
+ - **Dogfooding**: Uses official Gravito modules (@gravito/atlas, @gravito/stasis).
8
+ - **Decoupled**: Inter-satellite communication happens via Contracts and Events.
9
+
10
+ ## Layers
11
+ - **Domain**: Pure business rules.
12
+ - **Application**: Orchestration of domain tasks.
13
+ - **Infrastructure**: Implementation of persistence and external services.
14
+ - **Interface**: HTTP and Event entry points.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @gravito/satellite-catalog
2
+
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+ - @gravito/atlas@1.0.1
9
+ - @gravito/core@1.0.0
10
+ - @gravito/enterprise@1.0.0
11
+ - @gravito/signal@1.0.0
12
+ - @gravito/stasis@1.0.0
package/Dockerfile ADDED
@@ -0,0 +1,25 @@
1
+ FROM oven/bun:1.0 AS base
2
+ WORKDIR /usr/src/app
3
+
4
+ # Install dependencies
5
+ FROM base AS install
6
+ RUN mkdir -p /temp/dev
7
+ COPY package.json bun.lockb /temp/dev/
8
+ RUN cd /temp/dev && bun install --frozen-lockfile
9
+
10
+ # Build application
11
+ FROM base AS build
12
+ COPY --from=install /temp/dev/node_modules node_modules
13
+ COPY . .
14
+ ENV NODE_ENV=production
15
+ RUN bun run build
16
+
17
+ # Final production image
18
+ FROM base AS release
19
+ COPY --from=build /usr/src/app/dist/bootstrap.js index.js
20
+ COPY --from=build /usr/src/app/package.json .
21
+
22
+ # Create a non-root user for security
23
+ USER bun
24
+ EXPOSE 3000/tcp
25
+ ENTRYPOINT [ "bun", "run", "index.js" ]
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # 🛰️ Gravito Satellite: Catalog
2
+
3
+ 這是 Gravito Galaxy Architecture 中的核心商品目錄插件。它負責管理商品的靜態定義、規格(SKUs)以及無限層級的分類樹。
4
+
5
+ ## 🌟 核心功能
6
+
7
+ - **📦 結構化商品模型**: 區分商品主體 (Product) 與 規格變體 (Variants/SKUs)。
8
+ - **🌲 智慧型分類樹**: 使用路徑列 (Materialized Path) 實作,支援無限層級分類,且在移動分類時自動同步所有子孫路徑。
9
+ - **🖼️ 媒體整合**: 完美整合 `@gravito/nebula`,自動將存儲 Key 解析為完整的 CDN/本地 URL。
10
+ - **🌐 多語系支援**: 名稱與描述預設支援 JSON 格式的 i18n 儲存。
11
+ - **🚀 高性能查詢**: 針對大數據量設計,利用索引路徑進行子分類商品的快速檢索。
12
+
13
+ ## 🛠️ API 接口
14
+
15
+ | 方法 | 路徑 | 說明 |
16
+ | :--- | :--- | :--- |
17
+ | `GET` | `/api/catalog/products` | 獲取商品列表 (含 SKU) |
18
+ | `GET` | `/api/catalog/products/:id` | 獲取單一商品詳情 |
19
+ | `POST` | `/api/catalog/products` | 建立商品與多個 SKU (原子化) |
20
+ | `GET` | `/api/catalog/categories` | 獲取完整的樹狀分類結構 |
21
+
22
+ ## 🏗️ 領域驅動設計 (DDD) 結構
23
+
24
+ - **Domain**: 包含 `Category`, `Product`, `Variant` 實體與業務規則。
25
+ - **Application**: 提供 `CreateProduct` 與 `UpdateCategory` (含路徑同步) UseCases。
26
+ - **Infrastructure**: 基於 `Atlas` 的倉儲實現,支援事務保護。
27
+ - **Interface**: 基於 `Photon` 的 RESTful 控制器。
28
+
29
+ ## ⚙️ 安裝與註冊
30
+
31
+ 在您的 `PlanetCore` 中註冊:
32
+
33
+ ```typescript
34
+ import { CatalogServiceProvider } from '@gravito/satellite-catalog';
35
+
36
+ await core.use(new CatalogServiceProvider());
37
+ ```
38
+
39
+ ## 🧪 驗證
40
+
41
+ 執行全系統校閱測試:
42
+ ```bash
43
+ cd satellites/catalog
44
+ bun tests/grand-review.ts
45
+ ```
46
+
47
+ ---
48
+ *Created by Gravito Core Team.*
package/WHITEPAPER.md ADDED
@@ -0,0 +1,20 @@
1
+ # Whitepaper: Gravito Catalog Satellite (Recursive Data Architecture)
2
+ **Version:** 1.0.0 | **Author:** Gravito Engineering Team
3
+
4
+ ## 1. Abstract
5
+ The Catalog Satellite provides a highly optimized structure for managing infinite product hierarchies and complex SKU relationships.
6
+
7
+ ## 2. Practical Application Scenarios
8
+
9
+ ### Case A: Global Department Store (Infinite Nesting)
10
+ - **Problem**: A client needs a category structure like `Electronics > Audio > Headphones > Wireless`. Standard SQL joins would be slow.
11
+ - **Solution**: Using the **Materialized Path** (`/1/5/23/42/`), we fetch the entire breadcrumb trail or all products in the "Audio" department with a single indexed string query.
12
+ - **Result**: Sub-millisecond response times for deep navigation.
13
+
14
+ ### Case B: Mass Re-categorization
15
+ - **Problem**: Moving 1,000 products from "Sale" to "Clearance" involves changing parent categories for large branches.
16
+ - **Solution**: The `UpdateCategory` UseCase recursively recalculates and updates all descendant paths in a single atomic operation.
17
+ - **Result**: Guaranteed tree integrity with no broken links.
18
+
19
+ ---
20
+ *Gravito Framework: Precision in Data, Speed in Execution.*
@@ -0,0 +1,8 @@
1
+ import { ServiceProvider, Container } from '@gravito/core';
2
+
3
+ declare class CatalogServiceProvider extends ServiceProvider {
4
+ register(container: Container): void;
5
+ boot(): void;
6
+ }
7
+
8
+ export { CatalogServiceProvider };
package/dist/index.js ADDED
@@ -0,0 +1,321 @@
1
+ // src/index.ts
2
+ import { ServiceProvider } from "@gravito/core";
3
+
4
+ // src/Application/UseCases/AdminListProducts.ts
5
+ import { UseCase } from "@gravito/enterprise";
6
+ var AdminListProducts = class extends UseCase {
7
+ constructor(repository) {
8
+ super();
9
+ this.repository = repository;
10
+ }
11
+ async execute() {
12
+ return await this.repository.findAll();
13
+ }
14
+ };
15
+
16
+ // src/Application/UseCases/RecoverStock.ts
17
+ import { DB } from "@gravito/atlas";
18
+ import { UseCase as UseCase2 } from "@gravito/enterprise";
19
+ var RecoverStock = class extends UseCase2 {
20
+ async execute(input) {
21
+ const { variantId, quantity } = input;
22
+ const affected = await DB.table("product_variants").where("id", variantId).update({
23
+ stock: DB.raw("stock + ?", [quantity]),
24
+ version: DB.raw("version + 1"),
25
+ updated_at: /* @__PURE__ */ new Date()
26
+ });
27
+ if (affected === 0) {
28
+ throw new Error(`Variant [${variantId}] not found during stock recovery`);
29
+ }
30
+ console.log(`[Catalog] Stock recovered for variant ${variantId}: +${quantity}`);
31
+ }
32
+ };
33
+
34
+ // src/Infrastructure/Persistence/AtlasProductRepository.ts
35
+ import { DB as DB2 } from "@gravito/atlas";
36
+
37
+ // src/Domain/Entities/Product.ts
38
+ import { Entity } from "@gravito/enterprise";
39
+ var Variant = class extends Entity {
40
+ constructor(id, props) {
41
+ super(id);
42
+ this.props = props;
43
+ }
44
+ // Getters
45
+ get sku() {
46
+ return this.props.sku;
47
+ }
48
+ get price() {
49
+ return this.props.price;
50
+ }
51
+ get stock() {
52
+ return this.props.stock;
53
+ }
54
+ get options() {
55
+ return this.props.options;
56
+ }
57
+ get metadata() {
58
+ return this.props.metadata || {};
59
+ }
60
+ reduceStock(quantity) {
61
+ if (this.props.stock < quantity) {
62
+ throw new Error("Insufficient stock");
63
+ }
64
+ this.props.stock -= quantity;
65
+ this.props.updatedAt = /* @__PURE__ */ new Date();
66
+ }
67
+ };
68
+ var Product = class _Product extends Entity {
69
+ constructor(id, props) {
70
+ super(id);
71
+ this.props = props;
72
+ }
73
+ static create(id, name, slug) {
74
+ return new _Product(id, {
75
+ name,
76
+ slug,
77
+ status: "active",
78
+ variants: [],
79
+ categoryIds: [],
80
+ createdAt: /* @__PURE__ */ new Date(),
81
+ updatedAt: /* @__PURE__ */ new Date()
82
+ });
83
+ }
84
+ static reconstitute(id, props) {
85
+ return new _Product(id, props);
86
+ }
87
+ // Getters
88
+ get name() {
89
+ return this.props.name;
90
+ }
91
+ get slug() {
92
+ return this.props.slug;
93
+ }
94
+ get thumbnail() {
95
+ return this.props.thumbnail;
96
+ }
97
+ get variants() {
98
+ return this.props.variants;
99
+ }
100
+ get categoryIds() {
101
+ return this.props.categoryIds;
102
+ }
103
+ get metadata() {
104
+ return this.props.metadata || {};
105
+ }
106
+ setThumbnail(key) {
107
+ this.props.thumbnail = key;
108
+ this.props.updatedAt = /* @__PURE__ */ new Date();
109
+ }
110
+ addVariant(variant) {
111
+ this.props.variants.push(variant);
112
+ this.props.updatedAt = /* @__PURE__ */ new Date();
113
+ }
114
+ assignToCategory(categoryId) {
115
+ if (!this.props.categoryIds.includes(categoryId)) {
116
+ this.props.categoryIds.push(categoryId);
117
+ this.props.updatedAt = /* @__PURE__ */ new Date();
118
+ }
119
+ }
120
+ };
121
+
122
+ // src/Infrastructure/Persistence/AtlasProductRepository.ts
123
+ var AtlasProductRepository = class {
124
+ productsTable = "products";
125
+ variantsTable = "product_variants";
126
+ pivotTable = "category_product";
127
+ async save(product) {
128
+ await DB2.transaction(async (db) => {
129
+ const productData = {
130
+ id: product.id,
131
+ name: JSON.stringify(product.name),
132
+ slug: product.slug,
133
+ // @ts-expect-error
134
+ description: product.props.description || null,
135
+ // @ts-expect-error
136
+ brand: product.props.brand || null,
137
+ // @ts-expect-error
138
+ status: product.props.status,
139
+ thumbnail: product.thumbnail || null,
140
+ // @ts-expect-error
141
+ created_at: product.props.createdAt,
142
+ // @ts-expect-error
143
+ updated_at: product.props.updatedAt,
144
+ metadata: JSON.stringify(product.metadata)
145
+ };
146
+ const exists = await db.table(this.productsTable).where("id", product.id).first();
147
+ if (exists) {
148
+ await db.table(this.productsTable).where("id", product.id).update(productData);
149
+ } else {
150
+ await db.table(this.productsTable).insert(productData);
151
+ }
152
+ await db.table(this.variantsTable).where("product_id", product.id).delete();
153
+ for (const variant of product.variants) {
154
+ await db.table(this.variantsTable).insert({
155
+ id: variant.id,
156
+ product_id: product.id,
157
+ sku: variant.sku,
158
+ // @ts-expect-error
159
+ name: variant.props.name,
160
+ price: variant.price,
161
+ // @ts-expect-error
162
+ compare_at_price: variant.props.compareAtPrice,
163
+ stock: variant.stock,
164
+ options: JSON.stringify(variant.options),
165
+ // @ts-expect-error
166
+ created_at: variant.props.createdAt,
167
+ // @ts-expect-error
168
+ updated_at: variant.props.updatedAt,
169
+ metadata: JSON.stringify(variant.metadata)
170
+ });
171
+ }
172
+ await db.table(this.pivotTable).where("product_id", product.id).delete();
173
+ for (const categoryId of product.categoryIds) {
174
+ await db.table(this.pivotTable).insert({
175
+ product_id: product.id,
176
+ category_id: categoryId
177
+ });
178
+ }
179
+ });
180
+ }
181
+ async findById(id) {
182
+ const row = await DB2.table(this.productsTable).where("id", id).first();
183
+ if (!row) {
184
+ return null;
185
+ }
186
+ const variantRows = await DB2.table(this.variantsTable).where("product_id", id).get();
187
+ const categoryRows = await DB2.table(this.pivotTable).where("product_id", id).get();
188
+ return this.mapToDomain(row, variantRows, categoryRows);
189
+ }
190
+ async findAll(_filters) {
191
+ const rows = await DB2.table(this.productsTable).get();
192
+ const products = [];
193
+ for (const row of rows) {
194
+ const variantRows = await DB2.table(this.variantsTable).where("product_id", row.id).get();
195
+ const categoryRows = await DB2.table(this.pivotTable).where("product_id", row.id).get();
196
+ products.push(this.mapToDomain(row, variantRows, categoryRows));
197
+ }
198
+ return products;
199
+ }
200
+ async delete(id) {
201
+ await DB2.transaction(async (db) => {
202
+ await db.table(this.pivotTable).where("product_id", id).delete();
203
+ await db.table(this.variantsTable).where("product_id", id).delete();
204
+ await db.table(this.productsTable).where("id", id).delete();
205
+ });
206
+ }
207
+ mapToDomain(row, variantRows, categoryRows) {
208
+ const variants = variantRows.map(
209
+ (v) => new Variant(v.id, {
210
+ productId: v.product_id,
211
+ sku: v.sku,
212
+ name: v.name,
213
+ price: v.price,
214
+ compareAtPrice: v.compare_at_price,
215
+ stock: v.stock,
216
+ options: JSON.parse(v.options),
217
+ createdAt: new Date(v.created_at),
218
+ updatedAt: new Date(v.updated_at || v.created_at),
219
+ metadata: v.metadata ? JSON.parse(v.metadata) : {}
220
+ })
221
+ );
222
+ return Product.reconstitute(row.id, {
223
+ name: JSON.parse(row.name),
224
+ slug: row.slug,
225
+ description: row.description,
226
+ brand: row.brand,
227
+ status: row.status,
228
+ thumbnail: row.thumbnail,
229
+ variants,
230
+ categoryIds: categoryRows.map((c) => c.category_id),
231
+ createdAt: new Date(row.created_at),
232
+ updatedAt: new Date(row.updated_at || row.created_at),
233
+ metadata: row.metadata ? JSON.parse(row.metadata) : {}
234
+ });
235
+ }
236
+ };
237
+
238
+ // src/Interface/Http/Controllers/AdminProductController.ts
239
+ var AdminProductController = class {
240
+ constructor(core) {
241
+ this.core = core;
242
+ }
243
+ /**
244
+ * GET /api/admin/v1/catalog/products
245
+ */
246
+ async index(ctx) {
247
+ try {
248
+ const useCase = this.core.container.make(
249
+ "catalog.usecase.adminListProducts"
250
+ );
251
+ const products = await useCase.execute();
252
+ return ctx.json(
253
+ products.map((p) => ({
254
+ id: p.id,
255
+ name: p.name,
256
+ price: p.props.price,
257
+ stock: p.props.stock,
258
+ status: p.props.status || "active"
259
+ }))
260
+ );
261
+ } catch (error) {
262
+ return ctx.json({ message: error.message }, 500);
263
+ }
264
+ }
265
+ /**
266
+ * PATCH /api/admin/v1/catalog/products/:id
267
+ */
268
+ async update(ctx) {
269
+ return ctx.json({ success: true });
270
+ }
271
+ };
272
+
273
+ // src/index.ts
274
+ var CatalogServiceProvider = class extends ServiceProvider {
275
+ register(container) {
276
+ container.singleton("catalog.repository.product", () => new AtlasProductRepository());
277
+ container.singleton("catalog.stock.recover", () => new RecoverStock());
278
+ container.bind(
279
+ "catalog.usecase.adminListProducts",
280
+ () => new AdminListProducts(container.make("catalog.repository.product"))
281
+ );
282
+ container.singleton(
283
+ "catalog.controller.adminProduct",
284
+ () => new AdminProductController(this.core)
285
+ );
286
+ }
287
+ boot() {
288
+ const core = this.core;
289
+ if (!core) {
290
+ return;
291
+ }
292
+ core.logger.info("\u{1F6F0}\uFE0F Satellite Catalog is operational");
293
+ const adminProductCtrl = core.container.make(
294
+ "catalog.controller.adminProduct"
295
+ );
296
+ core.router.prefix("/api/admin/v1/catalog").group((router) => {
297
+ router.get("/products", (ctx) => adminProductCtrl.index(ctx));
298
+ router.patch("/products/:id", (ctx) => adminProductCtrl.update(ctx));
299
+ });
300
+ core.hooks.addAction(
301
+ "payment:refund:succeeded",
302
+ async (payload) => {
303
+ const recoverStock = core.container.make("catalog.stock.recover");
304
+ try {
305
+ for (const item of payload.items) {
306
+ await recoverStock.execute({
307
+ variantId: item.variantId,
308
+ quantity: item.quantity
309
+ });
310
+ }
311
+ core.logger.info(`[Catalog] Inventory closure completed for order: ${payload.orderId}`);
312
+ } catch (error) {
313
+ core.logger.error(`[Catalog] Failed to recover stock: ${error.message}`);
314
+ }
315
+ }
316
+ );
317
+ }
318
+ };
319
+ export {
320
+ CatalogServiceProvider
321
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@gravito/satellite-catalog",
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup src/index.ts --format esm --dts --clean --external @gravito/atlas --external @gravito/enterprise --external @gravito/signal --external @gravito/stasis --external @gravito/core",
10
+ "test": "bun test",
11
+ "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck"
12
+ },
13
+ "dependencies": {
14
+ "@gravito/atlas": "workspace:*",
15
+ "@gravito/enterprise": "workspace:*",
16
+ "@gravito/signal": "workspace:*",
17
+ "@gravito/stasis": "workspace:*",
18
+ "@gravito/core": "workspace:*"
19
+ },
20
+ "devDependencies": {
21
+ "tsup": "^8.0.0",
22
+ "typescript": "^5.9.3"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/gravito-framework/gravito.git",
30
+ "directory": "satellites/catalog"
31
+ }
32
+ }
@@ -0,0 +1,49 @@
1
+ import type { Category } from '../../Domain/Entities/Category'
2
+
3
+ export interface CategoryDTO {
4
+ id: string
5
+ parentId: string | null
6
+ path: string | null
7
+ name: Record<string, string>
8
+ slug: string
9
+ sortOrder: number
10
+ children?: CategoryDTO[] // For tree view
11
+ }
12
+
13
+ export class CategoryMapper {
14
+ static toDTO(category: Category): CategoryDTO {
15
+ return {
16
+ id: category.id,
17
+ parentId: category.parentId,
18
+ path: category.path,
19
+ name: category.name,
20
+ slug: category.slug,
21
+ sortOrder: category.sortOrder,
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Helper to build a tree structure from a flat array of DTOs
27
+ */
28
+ static buildTree(dtos: CategoryDTO[]): CategoryDTO[] {
29
+ const map = new Map<string, CategoryDTO & { children: CategoryDTO[] }>()
30
+ const roots: CategoryDTO[] = []
31
+
32
+ // Initialize map
33
+ for (const dto of dtos) {
34
+ map.set(dto.id, { ...dto, children: [] })
35
+ }
36
+
37
+ // Link children to parents
38
+ for (const dto of dtos) {
39
+ const node = map.get(dto.id)!
40
+ if (dto.parentId && map.has(dto.parentId)) {
41
+ map.get(dto.parentId)?.children.push(node)
42
+ } else {
43
+ roots.push(node)
44
+ }
45
+ }
46
+
47
+ return roots.sort((a, b) => a.sortOrder - b.sortOrder)
48
+ }
49
+ }
@@ -0,0 +1,74 @@
1
+ import { app } from '@gravito/core'
2
+ import type { Product, Variant } from '../../Domain/Entities/Product'
3
+
4
+ export interface VariantDTO {
5
+ id: string
6
+ sku: string
7
+ name: string | null
8
+ price: number
9
+ compareAtPrice: number | null
10
+ stock: number
11
+ options: Record<string, string>
12
+ }
13
+
14
+ export interface ProductDTO {
15
+ id: string
16
+ name: Record<string, string>
17
+ slug: string
18
+ description?: string
19
+ brand?: string
20
+ status: string
21
+ thumbnailUrl?: string
22
+ variants: VariantDTO[]
23
+ categoryIds: string[]
24
+ createdAt: string
25
+ }
26
+
27
+ export class ProductMapper {
28
+ static toDTO(product: Product): ProductDTO {
29
+ let thumbnailUrl: string | undefined
30
+
31
+ if (product.thumbnail) {
32
+ try {
33
+ const storage = app().container.make('storage') as any
34
+ if (storage) {
35
+ thumbnailUrl = storage.getUrl(product.thumbnail)
36
+ }
37
+ } catch (_e) {
38
+ // Fallback to key if storage not available or core not booted
39
+ thumbnailUrl = `/storage/${product.thumbnail}`
40
+ }
41
+ }
42
+
43
+ return {
44
+ id: product.id,
45
+ name: product.name,
46
+ slug: product.slug,
47
+ // @ts-expect-error - access private props for mapping
48
+ description: product.props.description,
49
+ // @ts-expect-error
50
+ brand: product.props.brand,
51
+ // @ts-expect-error
52
+ status: product.props.status,
53
+ thumbnailUrl,
54
+ variants: product.variants.map((v) => this.variantToDTO(v)),
55
+ categoryIds: product.categoryIds,
56
+ // @ts-expect-error
57
+ createdAt: product.props.createdAt.toISOString(),
58
+ }
59
+ }
60
+
61
+ private static variantToDTO(variant: Variant): VariantDTO {
62
+ return {
63
+ id: variant.id,
64
+ sku: variant.sku,
65
+ // @ts-expect-error
66
+ name: variant.props.name,
67
+ price: variant.price,
68
+ // @ts-expect-error
69
+ compareAtPrice: variant.props.compareAtPrice,
70
+ stock: variant.stock,
71
+ options: variant.options,
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,13 @@
1
+ import { UseCase } from '@gravito/enterprise'
2
+ import type { IProductRepository } from '../../Domain/Contracts/ICatalogRepository'
3
+ import type { Product } from '../../Domain/Entities/Product'
4
+
5
+ export class AdminListProducts extends UseCase<void, Product[]> {
6
+ constructor(private repository: IProductRepository) {
7
+ super()
8
+ }
9
+
10
+ async execute(): Promise<Product[]> {
11
+ return await this.repository.findAll()
12
+ }
13
+ }