@hasna/microservices 0.0.9 → 0.0.11
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/bin/index.js +236 -36
- package/bin/mcp.js +153 -4
- package/dist/index.js +120 -3
- package/microservices/microservice-analytics/package.json +27 -0
- package/microservices/microservice-analytics/src/cli/index.ts +373 -0
- package/microservices/microservice-analytics/src/db/analytics.ts +564 -0
- package/microservices/microservice-analytics/src/db/database.ts +93 -0
- package/microservices/microservice-analytics/src/db/migrations.ts +50 -0
- package/microservices/microservice-analytics/src/index.ts +37 -0
- package/microservices/microservice-analytics/src/mcp/index.ts +334 -0
- package/microservices/microservice-assets/package.json +27 -0
- package/microservices/microservice-assets/src/cli/index.ts +375 -0
- package/microservices/microservice-assets/src/db/assets.ts +370 -0
- package/microservices/microservice-assets/src/db/database.ts +93 -0
- package/microservices/microservice-assets/src/db/migrations.ts +51 -0
- package/microservices/microservice-assets/src/index.ts +32 -0
- package/microservices/microservice-assets/src/mcp/index.ts +346 -0
- package/microservices/microservice-compliance/package.json +27 -0
- package/microservices/microservice-compliance/src/cli/index.ts +467 -0
- package/microservices/microservice-compliance/src/db/compliance.ts +633 -0
- package/microservices/microservice-compliance/src/db/database.ts +93 -0
- package/microservices/microservice-compliance/src/db/migrations.ts +63 -0
- package/microservices/microservice-compliance/src/index.ts +46 -0
- package/microservices/microservice-compliance/src/mcp/index.ts +438 -0
- package/microservices/microservice-habits/package.json +27 -0
- package/microservices/microservice-habits/src/cli/index.ts +315 -0
- package/microservices/microservice-habits/src/db/database.ts +93 -0
- package/microservices/microservice-habits/src/db/habits.ts +451 -0
- package/microservices/microservice-habits/src/db/migrations.ts +46 -0
- package/microservices/microservice-habits/src/index.ts +31 -0
- package/microservices/microservice-habits/src/mcp/index.ts +313 -0
- package/microservices/microservice-health/package.json +27 -0
- package/microservices/microservice-health/src/cli/index.ts +484 -0
- package/microservices/microservice-health/src/db/database.ts +93 -0
- package/microservices/microservice-health/src/db/health.ts +708 -0
- package/microservices/microservice-health/src/db/migrations.ts +70 -0
- package/microservices/microservice-health/src/index.ts +63 -0
- package/microservices/microservice-health/src/mcp/index.ts +437 -0
- package/microservices/microservice-leads/package.json +27 -0
- package/microservices/microservice-leads/src/cli/index.ts +596 -0
- package/microservices/microservice-leads/src/db/database.ts +93 -0
- package/microservices/microservice-leads/src/db/leads.ts +520 -0
- package/microservices/microservice-leads/src/db/lists.ts +151 -0
- package/microservices/microservice-leads/src/db/migrations.ts +93 -0
- package/microservices/microservice-leads/src/index.ts +65 -0
- package/microservices/microservice-leads/src/lib/enrichment.ts +202 -0
- package/microservices/microservice-leads/src/lib/scoring.ts +134 -0
- package/microservices/microservice-leads/src/mcp/index.ts +533 -0
- package/microservices/microservice-notifications/package.json +27 -0
- package/microservices/microservice-notifications/src/cli/index.ts +349 -0
- package/microservices/microservice-notifications/src/db/database.ts +93 -0
- package/microservices/microservice-notifications/src/db/migrations.ts +62 -0
- package/microservices/microservice-notifications/src/db/notifications.ts +509 -0
- package/microservices/microservice-notifications/src/index.ts +41 -0
- package/microservices/microservice-notifications/src/mcp/index.ts +422 -0
- package/microservices/microservice-products/package.json +27 -0
- package/microservices/microservice-products/src/cli/index.ts +416 -0
- package/microservices/microservice-products/src/db/categories.ts +154 -0
- package/microservices/microservice-products/src/db/database.ts +93 -0
- package/microservices/microservice-products/src/db/migrations.ts +58 -0
- package/microservices/microservice-products/src/db/pricing-tiers.ts +66 -0
- package/microservices/microservice-products/src/db/products.ts +452 -0
- package/microservices/microservice-products/src/index.ts +53 -0
- package/microservices/microservice-products/src/mcp/index.ts +453 -0
- package/microservices/microservice-projects/package.json +27 -0
- package/microservices/microservice-projects/src/cli/index.ts +480 -0
- package/microservices/microservice-projects/src/db/database.ts +93 -0
- package/microservices/microservice-projects/src/db/migrations.ts +65 -0
- package/microservices/microservice-projects/src/db/projects.ts +715 -0
- package/microservices/microservice-projects/src/index.ts +57 -0
- package/microservices/microservice-projects/src/mcp/index.ts +501 -0
- package/microservices/microservice-proposals/package.json +27 -0
- package/microservices/microservice-proposals/src/cli/index.ts +400 -0
- package/microservices/microservice-proposals/src/db/database.ts +93 -0
- package/microservices/microservice-proposals/src/db/migrations.ts +52 -0
- package/microservices/microservice-proposals/src/db/proposals.ts +532 -0
- package/microservices/microservice-proposals/src/index.ts +37 -0
- package/microservices/microservice-proposals/src/mcp/index.ts +375 -0
- package/microservices/microservice-reading/package.json +27 -0
- package/microservices/microservice-reading/src/cli/index.ts +464 -0
- package/microservices/microservice-reading/src/db/database.ts +93 -0
- package/microservices/microservice-reading/src/db/migrations.ts +59 -0
- package/microservices/microservice-reading/src/db/reading.ts +524 -0
- package/microservices/microservice-reading/src/index.ts +51 -0
- package/microservices/microservice-reading/src/mcp/index.ts +368 -0
- package/microservices/microservice-travel/package.json +27 -0
- package/microservices/microservice-travel/src/cli/index.ts +505 -0
- package/microservices/microservice-travel/src/db/database.ts +93 -0
- package/microservices/microservice-travel/src/db/migrations.ts +77 -0
- package/microservices/microservice-travel/src/db/travel.ts +802 -0
- package/microservices/microservice-travel/src/index.ts +60 -0
- package/microservices/microservice-travel/src/mcp/index.ts +495 -0
- package/microservices/microservice-wiki/package.json +27 -0
- package/microservices/microservice-wiki/src/cli/index.ts +345 -0
- package/microservices/microservice-wiki/src/db/database.ts +93 -0
- package/microservices/microservice-wiki/src/db/migrations.ts +55 -0
- package/microservices/microservice-wiki/src/db/wiki.ts +395 -0
- package/microservices/microservice-wiki/src/index.ts +32 -0
- package/microservices/microservice-wiki/src/mcp/index.ts +344 -0
- package/package.json +1 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import {
|
|
5
|
+
createProduct,
|
|
6
|
+
getProduct,
|
|
7
|
+
listProducts,
|
|
8
|
+
updateProduct,
|
|
9
|
+
deleteProduct,
|
|
10
|
+
searchProducts,
|
|
11
|
+
getProductWithTiers,
|
|
12
|
+
bulkImportProducts,
|
|
13
|
+
exportProducts,
|
|
14
|
+
getProductStats,
|
|
15
|
+
} from "../db/products.js";
|
|
16
|
+
import {
|
|
17
|
+
createCategory,
|
|
18
|
+
listCategories,
|
|
19
|
+
getCategoryTree,
|
|
20
|
+
} from "../db/categories.js";
|
|
21
|
+
import {
|
|
22
|
+
createPricingTier,
|
|
23
|
+
listPricingTiers,
|
|
24
|
+
deletePricingTier,
|
|
25
|
+
} from "../db/pricing-tiers.js";
|
|
26
|
+
|
|
27
|
+
const program = new Command();
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.name("microservice-products")
|
|
31
|
+
.description("Product catalog microservice")
|
|
32
|
+
.version("0.0.1");
|
|
33
|
+
|
|
34
|
+
// --- Products ---
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("create")
|
|
38
|
+
.description("Create a new product")
|
|
39
|
+
.requiredOption("--name <name>", "Product name")
|
|
40
|
+
.option("--description <desc>", "Description")
|
|
41
|
+
.option("--type <type>", "Type (product/service/subscription/digital)", "product")
|
|
42
|
+
.option("--sku <sku>", "SKU")
|
|
43
|
+
.option("--price <price>", "Price")
|
|
44
|
+
.option("--currency <currency>", "Currency", "USD")
|
|
45
|
+
.option("--unit <unit>", "Unit of measure")
|
|
46
|
+
.option("--category <category>", "Category name")
|
|
47
|
+
.option("--status <status>", "Status (active/draft/archived)", "draft")
|
|
48
|
+
.option("--json", "Output as JSON", false)
|
|
49
|
+
.action((opts) => {
|
|
50
|
+
const product = createProduct({
|
|
51
|
+
name: opts.name,
|
|
52
|
+
description: opts.description,
|
|
53
|
+
type: opts.type,
|
|
54
|
+
sku: opts.sku,
|
|
55
|
+
price: opts.price ? parseFloat(opts.price) : undefined,
|
|
56
|
+
currency: opts.currency,
|
|
57
|
+
unit: opts.unit,
|
|
58
|
+
category: opts.category,
|
|
59
|
+
status: opts.status,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (opts.json) {
|
|
63
|
+
console.log(JSON.stringify(product, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
console.log(`Created product: ${product.name} (${product.id})`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command("get")
|
|
71
|
+
.description("Get a product by ID")
|
|
72
|
+
.argument("<id>", "Product ID")
|
|
73
|
+
.option("--json", "Output as JSON", false)
|
|
74
|
+
.action((id, opts) => {
|
|
75
|
+
const product = getProductWithTiers(id);
|
|
76
|
+
if (!product) {
|
|
77
|
+
console.error(`Product '${id}' not found.`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(product, null, 2));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(`${product.name}`);
|
|
85
|
+
if (product.sku) console.log(` SKU: ${product.sku}`);
|
|
86
|
+
console.log(` Type: ${product.type}`);
|
|
87
|
+
console.log(` Status: ${product.status}`);
|
|
88
|
+
if (product.price !== null) console.log(` Price: ${product.price} ${product.currency}`);
|
|
89
|
+
if (product.unit) console.log(` Unit: ${product.unit}`);
|
|
90
|
+
if (product.category) console.log(` Category: ${product.category}`);
|
|
91
|
+
if (product.description) console.log(` Description: ${product.description}`);
|
|
92
|
+
if (product.pricing_tiers.length > 0) {
|
|
93
|
+
console.log(` Pricing Tiers:`);
|
|
94
|
+
for (const t of product.pricing_tiers) {
|
|
95
|
+
console.log(` - ${t.name}: ${t.price} ${t.currency} (min qty: ${t.min_quantity})`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
program
|
|
102
|
+
.command("list")
|
|
103
|
+
.description("List products")
|
|
104
|
+
.option("--search <query>", "Search by name, description, or SKU")
|
|
105
|
+
.option("--category <category>", "Filter by category")
|
|
106
|
+
.option("--type <type>", "Filter by type")
|
|
107
|
+
.option("--status <status>", "Filter by status")
|
|
108
|
+
.option("--limit <n>", "Limit results")
|
|
109
|
+
.option("--json", "Output as JSON", false)
|
|
110
|
+
.action((opts) => {
|
|
111
|
+
const products = listProducts({
|
|
112
|
+
search: opts.search,
|
|
113
|
+
category: opts.category,
|
|
114
|
+
type: opts.type,
|
|
115
|
+
status: opts.status,
|
|
116
|
+
limit: opts.limit ? parseInt(opts.limit) : undefined,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
if (opts.json) {
|
|
120
|
+
console.log(JSON.stringify(products, null, 2));
|
|
121
|
+
} else {
|
|
122
|
+
if (products.length === 0) {
|
|
123
|
+
console.log("No products found.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const p of products) {
|
|
127
|
+
const sku = p.sku ? ` [${p.sku}]` : "";
|
|
128
|
+
const price = p.price !== null ? ` $${p.price}` : "";
|
|
129
|
+
console.log(` ${p.name}${sku}${price} (${p.status})`);
|
|
130
|
+
}
|
|
131
|
+
console.log(`\n${products.length} product(s)`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
program
|
|
136
|
+
.command("update")
|
|
137
|
+
.description("Update a product")
|
|
138
|
+
.argument("<id>", "Product ID")
|
|
139
|
+
.option("--name <name>", "Name")
|
|
140
|
+
.option("--description <desc>", "Description")
|
|
141
|
+
.option("--type <type>", "Type")
|
|
142
|
+
.option("--sku <sku>", "SKU")
|
|
143
|
+
.option("--price <price>", "Price")
|
|
144
|
+
.option("--currency <currency>", "Currency")
|
|
145
|
+
.option("--unit <unit>", "Unit")
|
|
146
|
+
.option("--category <category>", "Category")
|
|
147
|
+
.option("--status <status>", "Status")
|
|
148
|
+
.option("--json", "Output as JSON", false)
|
|
149
|
+
.action((id, opts) => {
|
|
150
|
+
const input: Record<string, unknown> = {};
|
|
151
|
+
if (opts.name !== undefined) input.name = opts.name;
|
|
152
|
+
if (opts.description !== undefined) input.description = opts.description;
|
|
153
|
+
if (opts.type !== undefined) input.type = opts.type;
|
|
154
|
+
if (opts.sku !== undefined) input.sku = opts.sku;
|
|
155
|
+
if (opts.price !== undefined) input.price = parseFloat(opts.price);
|
|
156
|
+
if (opts.currency !== undefined) input.currency = opts.currency;
|
|
157
|
+
if (opts.unit !== undefined) input.unit = opts.unit;
|
|
158
|
+
if (opts.category !== undefined) input.category = opts.category;
|
|
159
|
+
if (opts.status !== undefined) input.status = opts.status;
|
|
160
|
+
|
|
161
|
+
const product = updateProduct(id, input);
|
|
162
|
+
if (!product) {
|
|
163
|
+
console.error(`Product '${id}' not found.`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (opts.json) {
|
|
168
|
+
console.log(JSON.stringify(product, null, 2));
|
|
169
|
+
} else {
|
|
170
|
+
console.log(`Updated: ${product.name}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
program
|
|
175
|
+
.command("delete")
|
|
176
|
+
.description("Delete a product")
|
|
177
|
+
.argument("<id>", "Product ID")
|
|
178
|
+
.action((id) => {
|
|
179
|
+
const deleted = deleteProduct(id);
|
|
180
|
+
if (deleted) {
|
|
181
|
+
console.log(`Deleted product ${id}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.error(`Product '${id}' not found.`);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
program
|
|
189
|
+
.command("search")
|
|
190
|
+
.description("Search products")
|
|
191
|
+
.argument("<query>", "Search term")
|
|
192
|
+
.option("--json", "Output as JSON", false)
|
|
193
|
+
.action((query, opts) => {
|
|
194
|
+
const results = searchProducts(query);
|
|
195
|
+
|
|
196
|
+
if (opts.json) {
|
|
197
|
+
console.log(JSON.stringify(results, null, 2));
|
|
198
|
+
} else {
|
|
199
|
+
if (results.length === 0) {
|
|
200
|
+
console.log(`No products matching "${query}".`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const p of results) {
|
|
204
|
+
const sku = p.sku ? ` [${p.sku}]` : "";
|
|
205
|
+
console.log(` ${p.name}${sku} (${p.status})`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// --- Categories ---
|
|
211
|
+
|
|
212
|
+
const categoryCmd = program
|
|
213
|
+
.command("category")
|
|
214
|
+
.description("Category management");
|
|
215
|
+
|
|
216
|
+
categoryCmd
|
|
217
|
+
.command("create")
|
|
218
|
+
.description("Create a category")
|
|
219
|
+
.requiredOption("--name <name>", "Category name")
|
|
220
|
+
.option("--parent <id>", "Parent category ID")
|
|
221
|
+
.option("--description <desc>", "Description")
|
|
222
|
+
.option("--json", "Output as JSON", false)
|
|
223
|
+
.action((opts) => {
|
|
224
|
+
const category = createCategory({
|
|
225
|
+
name: opts.name,
|
|
226
|
+
parent_id: opts.parent,
|
|
227
|
+
description: opts.description,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
if (opts.json) {
|
|
231
|
+
console.log(JSON.stringify(category, null, 2));
|
|
232
|
+
} else {
|
|
233
|
+
console.log(`Created category: ${category.name} (${category.id})`);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
categoryCmd
|
|
238
|
+
.command("list")
|
|
239
|
+
.description("List categories")
|
|
240
|
+
.option("--search <query>", "Search")
|
|
241
|
+
.option("--json", "Output as JSON", false)
|
|
242
|
+
.action((opts) => {
|
|
243
|
+
const categories = listCategories({
|
|
244
|
+
search: opts.search,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (opts.json) {
|
|
248
|
+
console.log(JSON.stringify(categories, null, 2));
|
|
249
|
+
} else {
|
|
250
|
+
if (categories.length === 0) {
|
|
251
|
+
console.log("No categories found.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
for (const c of categories) {
|
|
255
|
+
const parent = c.parent_id ? ` (child of ${c.parent_id})` : "";
|
|
256
|
+
console.log(` ${c.name}${parent}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
categoryCmd
|
|
262
|
+
.command("tree")
|
|
263
|
+
.description("Display category tree")
|
|
264
|
+
.option("--json", "Output as JSON", false)
|
|
265
|
+
.action((opts) => {
|
|
266
|
+
const tree = getCategoryTree();
|
|
267
|
+
|
|
268
|
+
if (opts.json) {
|
|
269
|
+
console.log(JSON.stringify(tree, null, 2));
|
|
270
|
+
} else {
|
|
271
|
+
if (tree.length === 0) {
|
|
272
|
+
console.log("No categories found.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
function printTree(nodes: typeof tree, indent = 0): void {
|
|
276
|
+
for (const node of nodes) {
|
|
277
|
+
console.log(`${" ".repeat(indent)}${indent > 0 ? "|- " : ""}${node.name}`);
|
|
278
|
+
if (node.children.length > 0) {
|
|
279
|
+
printTree(node.children, indent + 1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
printTree(tree);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// --- Pricing Tiers ---
|
|
288
|
+
|
|
289
|
+
const tierCmd = program
|
|
290
|
+
.command("tier")
|
|
291
|
+
.description("Pricing tier management");
|
|
292
|
+
|
|
293
|
+
tierCmd
|
|
294
|
+
.command("add")
|
|
295
|
+
.description("Add a pricing tier to a product")
|
|
296
|
+
.requiredOption("--product <id>", "Product ID")
|
|
297
|
+
.requiredOption("--name <name>", "Tier name")
|
|
298
|
+
.requiredOption("--min-quantity <n>", "Minimum quantity")
|
|
299
|
+
.requiredOption("--price <price>", "Price")
|
|
300
|
+
.option("--currency <currency>", "Currency", "USD")
|
|
301
|
+
.option("--json", "Output as JSON", false)
|
|
302
|
+
.action((opts) => {
|
|
303
|
+
const tier = createPricingTier({
|
|
304
|
+
product_id: opts.product,
|
|
305
|
+
name: opts.name,
|
|
306
|
+
min_quantity: parseInt(opts.minQuantity),
|
|
307
|
+
price: parseFloat(opts.price),
|
|
308
|
+
currency: opts.currency,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
if (opts.json) {
|
|
312
|
+
console.log(JSON.stringify(tier, null, 2));
|
|
313
|
+
} else {
|
|
314
|
+
console.log(`Created tier: ${tier.name} (${tier.id})`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
tierCmd
|
|
319
|
+
.command("list")
|
|
320
|
+
.description("List pricing tiers for a product")
|
|
321
|
+
.argument("<product-id>", "Product ID")
|
|
322
|
+
.option("--json", "Output as JSON", false)
|
|
323
|
+
.action((productId, opts) => {
|
|
324
|
+
const tiers = listPricingTiers(productId);
|
|
325
|
+
|
|
326
|
+
if (opts.json) {
|
|
327
|
+
console.log(JSON.stringify(tiers, null, 2));
|
|
328
|
+
} else {
|
|
329
|
+
if (tiers.length === 0) {
|
|
330
|
+
console.log("No pricing tiers found.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const t of tiers) {
|
|
334
|
+
console.log(` ${t.name}: ${t.price} ${t.currency} (min qty: ${t.min_quantity})`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
tierCmd
|
|
340
|
+
.command("remove")
|
|
341
|
+
.description("Remove a pricing tier")
|
|
342
|
+
.argument("<id>", "Tier ID")
|
|
343
|
+
.action((id) => {
|
|
344
|
+
const deleted = deletePricingTier(id);
|
|
345
|
+
if (deleted) {
|
|
346
|
+
console.log(`Deleted tier ${id}`);
|
|
347
|
+
} else {
|
|
348
|
+
console.error(`Tier '${id}' not found.`);
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// --- Import/Export ---
|
|
354
|
+
|
|
355
|
+
program
|
|
356
|
+
.command("import")
|
|
357
|
+
.description("Bulk import products from CSV")
|
|
358
|
+
.argument("<csv>", "CSV string")
|
|
359
|
+
.option("--json", "Output as JSON", false)
|
|
360
|
+
.action((csv, opts) => {
|
|
361
|
+
const result = bulkImportProducts(csv);
|
|
362
|
+
|
|
363
|
+
if (opts.json) {
|
|
364
|
+
console.log(JSON.stringify(result, null, 2));
|
|
365
|
+
} else {
|
|
366
|
+
console.log(`Imported: ${result.imported}, Skipped: ${result.skipped}`);
|
|
367
|
+
if (result.errors.length > 0) {
|
|
368
|
+
console.log("Errors:");
|
|
369
|
+
for (const e of result.errors) {
|
|
370
|
+
console.log(` - ${e}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
program
|
|
377
|
+
.command("export")
|
|
378
|
+
.description("Export products")
|
|
379
|
+
.option("--format <format>", "Output format (csv/json)", "json")
|
|
380
|
+
.action((opts) => {
|
|
381
|
+
const output = exportProducts(opts.format);
|
|
382
|
+
console.log(output);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// --- Stats ---
|
|
386
|
+
|
|
387
|
+
program
|
|
388
|
+
.command("stats")
|
|
389
|
+
.description("Show product statistics")
|
|
390
|
+
.option("--json", "Output as JSON", false)
|
|
391
|
+
.action((opts) => {
|
|
392
|
+
const stats = getProductStats();
|
|
393
|
+
|
|
394
|
+
if (opts.json) {
|
|
395
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
396
|
+
} else {
|
|
397
|
+
console.log(`Total products: ${stats.total}`);
|
|
398
|
+
console.log("\nBy status:");
|
|
399
|
+
for (const [k, v] of Object.entries(stats.by_status)) {
|
|
400
|
+
console.log(` ${k}: ${v}`);
|
|
401
|
+
}
|
|
402
|
+
console.log("\nBy type:");
|
|
403
|
+
for (const [k, v] of Object.entries(stats.by_type)) {
|
|
404
|
+
console.log(` ${k}: ${v}`);
|
|
405
|
+
}
|
|
406
|
+
console.log("\nBy category:");
|
|
407
|
+
for (const [k, v] of Object.entries(stats.by_category)) {
|
|
408
|
+
console.log(` ${k}: ${v}`);
|
|
409
|
+
}
|
|
410
|
+
if (stats.avg_price !== null) {
|
|
411
|
+
console.log(`\nPrice range: ${stats.min_price} - ${stats.max_price} (avg: ${stats.avg_price?.toFixed(2)})`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category CRUD operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getDatabase } from "./database.js";
|
|
6
|
+
|
|
7
|
+
export interface Category {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
parent_id: string | null;
|
|
11
|
+
description: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CreateCategoryInput {
|
|
16
|
+
name: string;
|
|
17
|
+
parent_id?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createCategory(input: CreateCategoryInput): Category {
|
|
22
|
+
const db = getDatabase();
|
|
23
|
+
const id = crypto.randomUUID();
|
|
24
|
+
|
|
25
|
+
db.prepare(
|
|
26
|
+
`INSERT INTO categories (id, name, parent_id, description)
|
|
27
|
+
VALUES (?, ?, ?, ?)`
|
|
28
|
+
).run(id, input.name, input.parent_id || null, input.description || null);
|
|
29
|
+
|
|
30
|
+
return getCategory(id)!;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getCategory(id: string): Category | null {
|
|
34
|
+
const db = getDatabase();
|
|
35
|
+
return db.prepare("SELECT * FROM categories WHERE id = ?").get(id) as Category | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ListCategoriesOptions {
|
|
39
|
+
search?: string;
|
|
40
|
+
parent_id?: string | null;
|
|
41
|
+
limit?: number;
|
|
42
|
+
offset?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function listCategories(options: ListCategoriesOptions = {}): Category[] {
|
|
46
|
+
const db = getDatabase();
|
|
47
|
+
const conditions: string[] = [];
|
|
48
|
+
const params: unknown[] = [];
|
|
49
|
+
|
|
50
|
+
if (options.search) {
|
|
51
|
+
conditions.push("(name LIKE ? OR description LIKE ?)");
|
|
52
|
+
const q = `%${options.search}%`;
|
|
53
|
+
params.push(q, q);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (options.parent_id !== undefined) {
|
|
57
|
+
if (options.parent_id === null) {
|
|
58
|
+
conditions.push("parent_id IS NULL");
|
|
59
|
+
} else {
|
|
60
|
+
conditions.push("parent_id = ?");
|
|
61
|
+
params.push(options.parent_id);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let sql = "SELECT * FROM categories";
|
|
66
|
+
if (conditions.length > 0) {
|
|
67
|
+
sql += " WHERE " + conditions.join(" AND ");
|
|
68
|
+
}
|
|
69
|
+
sql += " ORDER BY name";
|
|
70
|
+
|
|
71
|
+
if (options.limit) {
|
|
72
|
+
sql += " LIMIT ?";
|
|
73
|
+
params.push(options.limit);
|
|
74
|
+
}
|
|
75
|
+
if (options.offset) {
|
|
76
|
+
sql += " OFFSET ?";
|
|
77
|
+
params.push(options.offset);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return db.prepare(sql).all(...params) as Category[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface UpdateCategoryInput {
|
|
84
|
+
name?: string;
|
|
85
|
+
parent_id?: string | null;
|
|
86
|
+
description?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function updateCategory(
|
|
90
|
+
id: string,
|
|
91
|
+
input: UpdateCategoryInput
|
|
92
|
+
): Category | null {
|
|
93
|
+
const db = getDatabase();
|
|
94
|
+
const existing = getCategory(id);
|
|
95
|
+
if (!existing) return null;
|
|
96
|
+
|
|
97
|
+
const sets: string[] = [];
|
|
98
|
+
const params: unknown[] = [];
|
|
99
|
+
|
|
100
|
+
if (input.name !== undefined) {
|
|
101
|
+
sets.push("name = ?");
|
|
102
|
+
params.push(input.name);
|
|
103
|
+
}
|
|
104
|
+
if (input.parent_id !== undefined) {
|
|
105
|
+
sets.push("parent_id = ?");
|
|
106
|
+
params.push(input.parent_id);
|
|
107
|
+
}
|
|
108
|
+
if (input.description !== undefined) {
|
|
109
|
+
sets.push("description = ?");
|
|
110
|
+
params.push(input.description);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (sets.length === 0) return existing;
|
|
114
|
+
|
|
115
|
+
params.push(id);
|
|
116
|
+
|
|
117
|
+
db.prepare(
|
|
118
|
+
`UPDATE categories SET ${sets.join(", ")} WHERE id = ?`
|
|
119
|
+
).run(...params);
|
|
120
|
+
|
|
121
|
+
return getCategory(id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function deleteCategory(id: string): boolean {
|
|
125
|
+
const db = getDatabase();
|
|
126
|
+
const result = db.prepare("DELETE FROM categories WHERE id = ?").run(id);
|
|
127
|
+
return result.changes > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface CategoryTreeNode extends Category {
|
|
131
|
+
children: CategoryTreeNode[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getCategoryTree(): CategoryTreeNode[] {
|
|
135
|
+
const all = listCategories();
|
|
136
|
+
const byId = new Map<string, CategoryTreeNode>();
|
|
137
|
+
const roots: CategoryTreeNode[] = [];
|
|
138
|
+
|
|
139
|
+
// Create nodes
|
|
140
|
+
for (const cat of all) {
|
|
141
|
+
byId.set(cat.id, { ...cat, children: [] });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Build tree
|
|
145
|
+
for (const node of byId.values()) {
|
|
146
|
+
if (node.parent_id && byId.has(node.parent_id)) {
|
|
147
|
+
byId.get(node.parent_id)!.children.push(node);
|
|
148
|
+
} else {
|
|
149
|
+
roots.push(node);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return roots;
|
|
154
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database connection for microservice-products
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Database } from "bun:sqlite";
|
|
6
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { MIGRATIONS } from "./migrations.js";
|
|
9
|
+
|
|
10
|
+
let _db: Database | null = null;
|
|
11
|
+
|
|
12
|
+
function getDbPath(): string {
|
|
13
|
+
// Environment variable override
|
|
14
|
+
if (process.env["MICROSERVICES_DIR"]) {
|
|
15
|
+
return join(process.env["MICROSERVICES_DIR"], "microservice-products", "data.db");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check for .microservices in current or parent directories
|
|
19
|
+
let dir = resolve(process.cwd());
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = join(dir, ".microservices", "microservice-products", "data.db");
|
|
22
|
+
const msDir = join(dir, ".microservices");
|
|
23
|
+
if (existsSync(msDir)) return candidate;
|
|
24
|
+
const parent = dirname(dir);
|
|
25
|
+
if (parent === dir) break;
|
|
26
|
+
dir = parent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Global fallback
|
|
30
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
31
|
+
return join(home, ".microservices", "microservice-products", "data.db");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensureDir(filePath: string): void {
|
|
35
|
+
const dir = dirname(resolve(filePath));
|
|
36
|
+
if (!existsSync(dir)) {
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getDatabase(): Database {
|
|
42
|
+
if (_db) return _db;
|
|
43
|
+
|
|
44
|
+
const dbPath = getDbPath();
|
|
45
|
+
ensureDir(dbPath);
|
|
46
|
+
|
|
47
|
+
_db = new Database(dbPath);
|
|
48
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
49
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
50
|
+
|
|
51
|
+
// Create migrations table
|
|
52
|
+
_db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
54
|
+
id INTEGER PRIMARY KEY,
|
|
55
|
+
name TEXT NOT NULL,
|
|
56
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
|
|
60
|
+
// Apply pending migrations
|
|
61
|
+
const applied = _db
|
|
62
|
+
.query("SELECT id FROM _migrations ORDER BY id")
|
|
63
|
+
.all() as { id: number }[];
|
|
64
|
+
const appliedIds = new Set(applied.map((r) => r.id));
|
|
65
|
+
|
|
66
|
+
for (const migration of MIGRATIONS) {
|
|
67
|
+
if (appliedIds.has(migration.id)) continue;
|
|
68
|
+
|
|
69
|
+
_db.exec("BEGIN");
|
|
70
|
+
try {
|
|
71
|
+
_db.exec(migration.sql);
|
|
72
|
+
_db.prepare("INSERT INTO _migrations (id, name) VALUES (?, ?)").run(
|
|
73
|
+
migration.id,
|
|
74
|
+
migration.name
|
|
75
|
+
);
|
|
76
|
+
_db.exec("COMMIT");
|
|
77
|
+
} catch (error) {
|
|
78
|
+
_db.exec("ROLLBACK");
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Migration ${migration.id} (${migration.name}) failed: ${error instanceof Error ? error.message : String(error)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return _db;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function closeDatabase(): void {
|
|
89
|
+
if (_db) {
|
|
90
|
+
_db.close();
|
|
91
|
+
_db = null;
|
|
92
|
+
}
|
|
93
|
+
}
|