nitrostack 1.0.0 → 1.0.2
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/CHANGELOG.md +30 -0
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/package.json +1 -1
- package/src/studio/README.md +140 -0
- package/src/studio/app/api/auth/fetch-metadata/route.ts +71 -0
- package/src/studio/app/api/auth/register-client/route.ts +67 -0
- package/src/studio/app/api/chat/route.ts +123 -0
- package/src/studio/app/api/health/checks/route.ts +42 -0
- package/src/studio/app/api/health/route.ts +13 -0
- package/src/studio/app/api/init/route.ts +85 -0
- package/src/studio/app/api/ping/route.ts +13 -0
- package/src/studio/app/api/prompts/[name]/route.ts +21 -0
- package/src/studio/app/api/prompts/route.ts +13 -0
- package/src/studio/app/api/resources/[...uri]/route.ts +18 -0
- package/src/studio/app/api/resources/route.ts +13 -0
- package/src/studio/app/api/roots/route.ts +13 -0
- package/src/studio/app/api/sampling/route.ts +14 -0
- package/src/studio/app/api/tools/[name]/call/route.ts +41 -0
- package/src/studio/app/api/tools/route.ts +23 -0
- package/src/studio/app/api/widget-examples/route.ts +44 -0
- package/src/studio/app/auth/callback/page.tsx +160 -0
- package/src/studio/app/auth/page.tsx +543 -0
- package/src/studio/app/chat/page.tsx +530 -0
- package/src/studio/app/chat/page.tsx.backup +390 -0
- package/src/studio/app/globals.css +410 -0
- package/src/studio/app/health/page.tsx +177 -0
- package/src/studio/app/layout.tsx +48 -0
- package/src/studio/app/page.tsx +337 -0
- package/src/studio/app/page.tsx.backup +346 -0
- package/src/studio/app/ping/page.tsx +204 -0
- package/src/studio/app/prompts/page.tsx +228 -0
- package/src/studio/app/resources/page.tsx +313 -0
- package/src/studio/components/EnlargeModal.tsx +116 -0
- package/src/studio/components/Sidebar.tsx +133 -0
- package/src/studio/components/ToolCard.tsx +108 -0
- package/src/studio/components/WidgetRenderer.tsx +99 -0
- package/src/studio/lib/api.ts +207 -0
- package/src/studio/lib/llm-service.ts +361 -0
- package/src/studio/lib/mcp-client.ts +168 -0
- package/src/studio/lib/store.ts +192 -0
- package/src/studio/lib/theme-provider.tsx +50 -0
- package/src/studio/lib/types.ts +107 -0
- package/src/studio/lib/widget-loader.ts +90 -0
- package/src/studio/middleware.ts +27 -0
- package/src/studio/next.config.js +16 -0
- package/src/studio/package-lock.json +2696 -0
- package/src/studio/package.json +34 -0
- package/src/studio/postcss.config.mjs +10 -0
- package/src/studio/tailwind.config.ts +67 -0
- package/src/studio/tsconfig.json +41 -0
- package/templates/typescript-auth/.env.example +23 -0
- package/templates/typescript-auth/src/app.module.ts +103 -0
- package/templates/typescript-auth/src/db/database.ts +163 -0
- package/templates/typescript-auth/src/db/seed.ts +374 -0
- package/templates/typescript-auth/src/db/setup.ts +87 -0
- package/templates/typescript-auth/src/events/analytics.service.ts +52 -0
- package/templates/typescript-auth/src/events/notification.service.ts +40 -0
- package/templates/typescript-auth/src/filters/global-exception.filter.ts +28 -0
- package/templates/typescript-auth/src/guards/README.md +75 -0
- package/templates/typescript-auth/src/guards/jwt.guard.ts +105 -0
- package/templates/typescript-auth/src/health/database.health.ts +41 -0
- package/templates/typescript-auth/src/index.ts +26 -0
- package/templates/typescript-auth/src/interceptors/transform.interceptor.ts +24 -0
- package/templates/typescript-auth/src/middleware/logging.middleware.ts +42 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.module.ts +16 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.prompts.ts +114 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.resources.ts +40 -0
- package/templates/typescript-auth/src/modules/addresses/addresses.tools.ts +241 -0
- package/templates/typescript-auth/src/modules/auth/auth.module.ts +16 -0
- package/templates/typescript-auth/src/modules/auth/auth.prompts.ts +147 -0
- package/templates/typescript-auth/src/modules/auth/auth.resources.ts +84 -0
- package/templates/typescript-auth/src/modules/auth/auth.tools.ts +139 -0
- package/templates/typescript-auth/src/modules/cart/cart.module.ts +16 -0
- package/templates/typescript-auth/src/modules/cart/cart.prompts.ts +95 -0
- package/templates/typescript-auth/src/modules/cart/cart.resources.ts +44 -0
- package/templates/typescript-auth/src/modules/cart/cart.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/orders/orders.module.ts +16 -0
- package/templates/typescript-auth/src/modules/orders/orders.prompts.ts +88 -0
- package/templates/typescript-auth/src/modules/orders/orders.resources.ts +48 -0
- package/templates/typescript-auth/src/modules/orders/orders.tools.ts +281 -0
- package/templates/typescript-auth/src/modules/products/products.module.ts +16 -0
- package/templates/typescript-auth/src/modules/products/products.prompts.ts +146 -0
- package/templates/typescript-auth/src/modules/products/products.resources.ts +98 -0
- package/templates/typescript-auth/src/modules/products/products.tools.ts +266 -0
- package/templates/typescript-auth/src/pipes/validation.pipe.ts +42 -0
- package/templates/typescript-auth/src/services/database.service.ts +90 -0
- package/templates/typescript-auth/src/widgets/app/add-to-cart/page.tsx +122 -0
- package/templates/typescript-auth/src/widgets/app/address-added/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/address-deleted/page.tsx +105 -0
- package/templates/typescript-auth/src/widgets/app/address-list/page.tsx +139 -0
- package/templates/typescript-auth/src/widgets/app/address-updated/page.tsx +153 -0
- package/templates/typescript-auth/src/widgets/app/cart-cleared/page.tsx +86 -0
- package/templates/typescript-auth/src/widgets/app/cart-updated/page.tsx +116 -0
- package/templates/typescript-auth/src/widgets/app/categories/page.tsx +134 -0
- package/templates/typescript-auth/src/widgets/app/layout.tsx +21 -0
- package/templates/typescript-auth/src/widgets/app/login-result/page.tsx +129 -0
- package/templates/typescript-auth/src/widgets/app/order-confirmation/page.tsx +206 -0
- package/templates/typescript-auth/src/widgets/app/order-details/page.tsx +225 -0
- package/templates/typescript-auth/src/widgets/app/order-history/page.tsx +218 -0
- package/templates/typescript-auth/src/widgets/app/product-card/page.tsx +121 -0
- package/templates/typescript-auth/src/widgets/app/products-grid/page.tsx +173 -0
- package/templates/typescript-auth/src/widgets/app/shopping-cart/page.tsx +187 -0
- package/templates/typescript-auth/src/widgets/app/whoami/page.tsx +165 -0
- package/templates/typescript-auth/src/widgets/next.config.js +38 -0
- package/templates/typescript-auth/src/widgets/package.json +18 -0
- package/templates/typescript-auth/src/widgets/styles/ecommerce.ts +169 -0
- package/templates/typescript-auth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth/src/widgets/types/tool-data.ts +141 -0
- package/templates/typescript-auth/src/widgets/widget-manifest.json +464 -0
- package/templates/typescript-auth/tsconfig.json +27 -0
- package/templates/typescript-auth-api-key/.env +15 -0
- package/templates/typescript-auth-api-key/.env.example +4 -0
- package/templates/typescript-auth-api-key/src/app.module.ts +38 -0
- package/templates/typescript-auth-api-key/src/guards/apikey.guard.ts +47 -0
- package/templates/typescript-auth-api-key/src/guards/multi-auth.guard.ts +157 -0
- package/templates/typescript-auth-api-key/src/health/system.health.ts +55 -0
- package/templates/typescript-auth-api-key/src/index.ts +47 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-auth-api-key/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.module.ts +18 -0
- package/templates/typescript-auth-api-key/src/modules/demo/demo.tools.ts +155 -0
- package/templates/typescript-auth-api-key/src/modules/demo/multi-auth.tools.ts +123 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-auth-api-key/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-auth-api-key/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-auth-api-key/src/widgets/next.config.js +37 -0
- package/templates/typescript-auth-api-key/src/widgets/package.json +24 -0
- package/templates/typescript-auth-api-key/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-auth-api-key/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-auth-api-key/tsconfig.json +23 -0
- package/templates/typescript-oauth/.env.example +91 -0
- package/templates/typescript-oauth/src/app.module.ts +89 -0
- package/templates/typescript-oauth/src/guards/oauth.guard.ts +127 -0
- package/templates/typescript-oauth/src/index.ts +74 -0
- package/templates/typescript-oauth/src/modules/demo/demo.module.ts +16 -0
- package/templates/typescript-oauth/src/modules/demo/demo.tools.ts +190 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-oauth/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-oauth/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-oauth/src/widgets/next.config.js +37 -0
- package/templates/typescript-oauth/src/widgets/package.json +24 -0
- package/templates/typescript-oauth/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-oauth/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-oauth/tsconfig.json +23 -0
- package/templates/typescript-starter/.env.example +4 -0
- package/templates/typescript-starter/src/app.module.ts +34 -0
- package/templates/typescript-starter/src/health/system.health.ts +55 -0
- package/templates/typescript-starter/src/index.ts +27 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.module.ts +12 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.prompts.ts +73 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.resources.ts +60 -0
- package/templates/typescript-starter/src/modules/calculator/calculator.tools.ts +71 -0
- package/templates/typescript-starter/src/widgets/app/calculator-operations/page.tsx +133 -0
- package/templates/typescript-starter/src/widgets/app/calculator-result/page.tsx +134 -0
- package/templates/typescript-starter/src/widgets/app/layout.tsx +14 -0
- package/templates/typescript-starter/src/widgets/next.config.js +37 -0
- package/templates/typescript-starter/src/widgets/package.json +24 -0
- package/templates/typescript-starter/src/widgets/tsconfig.json +28 -0
- package/templates/typescript-starter/src/widgets/widget-manifest.json +48 -0
- package/templates/typescript-starter/tsconfig.json +23 -0
- package/LICENSE_URLS_UPDATE_COMPLETE.md +0 -388
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ToolDecorator as Tool,
|
|
3
|
+
Widget,
|
|
4
|
+
z,
|
|
5
|
+
ExecutionContext,
|
|
6
|
+
Injectable,
|
|
7
|
+
UseMiddleware,
|
|
8
|
+
UseInterceptors,
|
|
9
|
+
UsePipes,
|
|
10
|
+
UseFilters,
|
|
11
|
+
Cache,
|
|
12
|
+
RateLimit,
|
|
13
|
+
} from 'nitrostack';
|
|
14
|
+
import { DatabaseService } from '../../services/database.service.js';
|
|
15
|
+
import { LoggingMiddleware } from '../../middleware/logging.middleware.js';
|
|
16
|
+
import { TransformInterceptor } from '../../interceptors/transform.interceptor.js';
|
|
17
|
+
import { ValidationPipe } from '../../pipes/validation.pipe.js';
|
|
18
|
+
import { GlobalExceptionFilter } from '../../filters/global-exception.filter.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Products Tools
|
|
22
|
+
*
|
|
23
|
+
* Demonstrates all V3.1 features:
|
|
24
|
+
* - Dependency Injection (@Injectable)
|
|
25
|
+
* - Middleware (@UseMiddleware)
|
|
26
|
+
* - Interceptors (@UseInterceptors)
|
|
27
|
+
* - Pipes (@UsePipes)
|
|
28
|
+
* - Exception Filters (@UseFilters)
|
|
29
|
+
* - Caching (@Cache)
|
|
30
|
+
* - Rate Limiting (@RateLimit)
|
|
31
|
+
*/
|
|
32
|
+
@Injectable()
|
|
33
|
+
export class ProductsTools {
|
|
34
|
+
constructor(private database: DatabaseService) {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Browse products - No authentication required
|
|
38
|
+
* Demonstrates: Middleware, Interceptors, Pipes, Caching
|
|
39
|
+
*/
|
|
40
|
+
@Tool({
|
|
41
|
+
name: 'browse_products',
|
|
42
|
+
description: 'Browse available products. Can filter by category, search by name, and paginate results.',
|
|
43
|
+
inputSchema: z.object({
|
|
44
|
+
category: z.enum(['Electronics', 'Clothing', 'Home', 'Books', 'Sports', 'All']).default('All').describe('Filter by product category'),
|
|
45
|
+
search: z.string().optional().describe('Search products by name'),
|
|
46
|
+
page: z.number().min(1).default(1).describe('Page number for pagination'),
|
|
47
|
+
limit: z.number().min(1).max(50).default(10).describe('Number of products per page'),
|
|
48
|
+
}),
|
|
49
|
+
examples: {
|
|
50
|
+
request: {
|
|
51
|
+
category: 'Beauty',
|
|
52
|
+
page: 1,
|
|
53
|
+
limit: 4
|
|
54
|
+
},
|
|
55
|
+
response: {
|
|
56
|
+
products: [
|
|
57
|
+
{
|
|
58
|
+
id: 'prod-1',
|
|
59
|
+
name: 'Essence Mascara Lash Princess',
|
|
60
|
+
description: 'The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects.',
|
|
61
|
+
price: 9.99,
|
|
62
|
+
category: 'Beauty',
|
|
63
|
+
stock: 99,
|
|
64
|
+
image_url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'prod-2',
|
|
68
|
+
name: 'Eyeshadow Palette with Mirror',
|
|
69
|
+
description: 'The Eyeshadow Palette offers a versatile range of eyeshadow shades for creating stunning eye looks.',
|
|
70
|
+
price: 19.99,
|
|
71
|
+
category: 'Beauty',
|
|
72
|
+
stock: 34,
|
|
73
|
+
image_url: 'https://cdn.dummyjson.com/product-images/beauty/eyeshadow-palette-with-mirror/thumbnail.webp'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'prod-3',
|
|
77
|
+
name: 'Powder Canister',
|
|
78
|
+
description: 'The Powder Canister is a finely milled setting powder designed to set makeup and control shine.',
|
|
79
|
+
price: 14.99,
|
|
80
|
+
category: 'Beauty',
|
|
81
|
+
stock: 89,
|
|
82
|
+
image_url: 'https://cdn.dummyjson.com/product-images/beauty/powder-canister/thumbnail.webp'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'prod-4',
|
|
86
|
+
name: 'Red Lipstick',
|
|
87
|
+
description: 'The Red Lipstick is a classic and bold choice for adding a pop of color to your lips.',
|
|
88
|
+
price: 12.99,
|
|
89
|
+
category: 'Beauty',
|
|
90
|
+
stock: 91,
|
|
91
|
+
image_url: 'https://cdn.dummyjson.com/product-images/beauty/red-lipstick/thumbnail.webp'
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
pagination: {
|
|
95
|
+
page: 1,
|
|
96
|
+
limit: 4,
|
|
97
|
+
totalPages: 2,
|
|
98
|
+
totalResults: 5
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
@Widget('products-grid')
|
|
104
|
+
@UseMiddleware(LoggingMiddleware)
|
|
105
|
+
@UseInterceptors(TransformInterceptor)
|
|
106
|
+
@UsePipes(ValidationPipe)
|
|
107
|
+
@UseFilters(GlobalExceptionFilter)
|
|
108
|
+
@Cache({ ttl: 60, key: (input) => `products:${input.category}:${input.search}:${input.page}` })
|
|
109
|
+
async browseProducts(input: any, context: ExecutionContext) {
|
|
110
|
+
// Apply defaults if not provided
|
|
111
|
+
const category = input.category || 'All';
|
|
112
|
+
const page = input.page || 1;
|
|
113
|
+
const limit = input.limit || 10;
|
|
114
|
+
const search = input.search || '';
|
|
115
|
+
|
|
116
|
+
const offset = (page - 1) * limit;
|
|
117
|
+
|
|
118
|
+
let query = 'SELECT * FROM products WHERE 1=1';
|
|
119
|
+
const params: any[] = [];
|
|
120
|
+
|
|
121
|
+
// Filter by category
|
|
122
|
+
if (category && category !== 'All') {
|
|
123
|
+
query += ' AND category = ?';
|
|
124
|
+
params.push(category);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Search by name
|
|
128
|
+
if (search) {
|
|
129
|
+
query += ' AND name LIKE ?';
|
|
130
|
+
params.push(`%${search}%`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Add pagination
|
|
134
|
+
query += ' ORDER BY name LIMIT ? OFFSET ?';
|
|
135
|
+
params.push(limit, offset);
|
|
136
|
+
|
|
137
|
+
const products = this.database.query(query, params);
|
|
138
|
+
|
|
139
|
+
// Get total count
|
|
140
|
+
let countQuery = 'SELECT COUNT(*) as count FROM products WHERE 1=1';
|
|
141
|
+
const countParams: any[] = [];
|
|
142
|
+
|
|
143
|
+
if (category && category !== 'All') {
|
|
144
|
+
countQuery += ' AND category = ?';
|
|
145
|
+
countParams.push(category);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (search) {
|
|
149
|
+
countQuery += ' AND name LIKE ?';
|
|
150
|
+
countParams.push(`%${search}%`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const result = this.database.queryOne<{ count: number }>(countQuery, countParams);
|
|
154
|
+
const count = result?.count || 0;
|
|
155
|
+
const totalPages = Math.ceil(count / limit);
|
|
156
|
+
|
|
157
|
+
context.logger.info(`Browsing products: category=${category}, search=${search}, page=${page}, found=${products.length} products`);
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
products,
|
|
161
|
+
pagination: {
|
|
162
|
+
page,
|
|
163
|
+
limit,
|
|
164
|
+
total: count,
|
|
165
|
+
totalPages,
|
|
166
|
+
hasMore: page < totalPages,
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get product details - No authentication required
|
|
173
|
+
* Demonstrates: All features + Rate Limiting
|
|
174
|
+
*/
|
|
175
|
+
@Tool({
|
|
176
|
+
name: 'get_product_details',
|
|
177
|
+
description: 'Get detailed information about a specific product by ID.',
|
|
178
|
+
inputSchema: z.object({
|
|
179
|
+
product_id: z.string().describe('The ID of the product to retrieve'),
|
|
180
|
+
}),
|
|
181
|
+
examples: {
|
|
182
|
+
request: {
|
|
183
|
+
product_id: 'prod-1'
|
|
184
|
+
},
|
|
185
|
+
response: {
|
|
186
|
+
product: {
|
|
187
|
+
id: 'prod-1',
|
|
188
|
+
name: 'Essence Mascara Lash Princess',
|
|
189
|
+
description: 'The Essence Mascara Lash Princess is a popular mascara known for its volumizing and lengthening effects.',
|
|
190
|
+
price: 9.99,
|
|
191
|
+
category: 'Beauty',
|
|
192
|
+
stock: 99,
|
|
193
|
+
image_url: 'https://cdn.dummyjson.com/product-images/beauty/essence-mascara-lash-princess/thumbnail.webp'
|
|
194
|
+
},
|
|
195
|
+
availability: 'In Stock',
|
|
196
|
+
stockMessage: 'Available'
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
@Widget('product-card')
|
|
201
|
+
@UseMiddleware(LoggingMiddleware)
|
|
202
|
+
@UseInterceptors(TransformInterceptor)
|
|
203
|
+
@UsePipes(ValidationPipe)
|
|
204
|
+
@UseFilters(GlobalExceptionFilter)
|
|
205
|
+
@Cache({ ttl: 300, key: (input) => `product:${input.product_id}` }) // Cache for 5 minutes
|
|
206
|
+
@RateLimit({ requests: 100, window: '1m' }) // 100 requests per minute
|
|
207
|
+
async getProductDetails(input: any, context: ExecutionContext) {
|
|
208
|
+
const product = this.database.queryOne(`SELECT * FROM products WHERE id = ?`, [input.product_id]);
|
|
209
|
+
|
|
210
|
+
if (!product) {
|
|
211
|
+
throw new Error(`Product with ID ${input.product_id} not found`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
context.logger.info(`Retrieved details for product: ${(product as any).name}`);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
product,
|
|
218
|
+
availability: (product as any).stock > 0 ? 'In Stock' : 'Out of Stock',
|
|
219
|
+
stockMessage: (product as any).stock > 10
|
|
220
|
+
? 'Available'
|
|
221
|
+
: (product as any).stock > 0
|
|
222
|
+
? `Only ${(product as any).stock} left!`
|
|
223
|
+
: 'Currently unavailable',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get product categories - No authentication required
|
|
229
|
+
* Demonstrates: Simple caching
|
|
230
|
+
*/
|
|
231
|
+
@Tool({
|
|
232
|
+
name: 'get_categories',
|
|
233
|
+
description: 'Get all available product categories.',
|
|
234
|
+
inputSchema: z.object({}),
|
|
235
|
+
examples: {
|
|
236
|
+
request: {},
|
|
237
|
+
response: {
|
|
238
|
+
categories: [
|
|
239
|
+
{ name: 'Beauty', productCount: 5 },
|
|
240
|
+
{ name: 'Fragrances', productCount: 5 },
|
|
241
|
+
{ name: 'Furniture', productCount: 5 },
|
|
242
|
+
{ name: 'Groceries', productCount: 5 }
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
@Widget('categories')
|
|
248
|
+
@UseMiddleware(LoggingMiddleware)
|
|
249
|
+
@UseInterceptors(TransformInterceptor)
|
|
250
|
+
@Cache({ ttl: 300 }) // Cache for 5 minutes
|
|
251
|
+
async getCategories(input: any, context: ExecutionContext) {
|
|
252
|
+
const categories = this.database.query(`
|
|
253
|
+
SELECT DISTINCT category, COUNT(*) as count
|
|
254
|
+
FROM products
|
|
255
|
+
GROUP BY category
|
|
256
|
+
ORDER BY category
|
|
257
|
+
`);
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
categories: categories.map((c: any) => ({
|
|
261
|
+
name: c.category,
|
|
262
|
+
productCount: c.count,
|
|
263
|
+
})),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Pipe, PipeInterface, ArgumentMetadata } from 'nitrostack';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation Pipe
|
|
5
|
+
*
|
|
6
|
+
* Validates and sanitizes input data
|
|
7
|
+
*/
|
|
8
|
+
@Pipe()
|
|
9
|
+
export class ValidationPipe implements PipeInterface {
|
|
10
|
+
transform(value: any, metadata: ArgumentMetadata) {
|
|
11
|
+
if (!value || typeof value !== 'object') {
|
|
12
|
+
throw new Error('Input must be an object');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Sanitize strings
|
|
16
|
+
const sanitized = this.sanitizeObject(value);
|
|
17
|
+
|
|
18
|
+
return sanitized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private sanitizeObject(obj: any): any {
|
|
22
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
23
|
+
return obj;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sanitized: any = Array.isArray(obj) ? [] : {};
|
|
27
|
+
|
|
28
|
+
for (const key in obj) {
|
|
29
|
+
if (typeof obj[key] === 'string') {
|
|
30
|
+
// Trim and sanitize strings
|
|
31
|
+
sanitized[key] = obj[key].trim();
|
|
32
|
+
} else if (typeof obj[key] === 'object') {
|
|
33
|
+
sanitized[key] = this.sanitizeObject(obj[key]);
|
|
34
|
+
} else {
|
|
35
|
+
sanitized[key] = obj[key];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return sanitized;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Injectable } from 'nitrostack';
|
|
2
|
+
import Database from 'better-sqlite3';
|
|
3
|
+
import { getDatabase } from '../db/database.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database Service
|
|
7
|
+
*
|
|
8
|
+
* Provides database access with dependency injection
|
|
9
|
+
*/
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class DatabaseService {
|
|
12
|
+
private db: Database.Database;
|
|
13
|
+
|
|
14
|
+
constructor() {
|
|
15
|
+
this.db = getDatabase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a query
|
|
20
|
+
*/
|
|
21
|
+
query<T = any>(sql: string, params: any[] = []): T[] {
|
|
22
|
+
const stmt = this.db.prepare(sql);
|
|
23
|
+
// better-sqlite3 expects individual parameters, not an array
|
|
24
|
+
if (params.length === 0) {
|
|
25
|
+
return stmt.all() as T[];
|
|
26
|
+
}
|
|
27
|
+
return stmt.all(...params) as T[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute a query and get first result
|
|
32
|
+
*/
|
|
33
|
+
queryOne<T = any>(sql: string, params: any[] = []): T | null {
|
|
34
|
+
const stmt = this.db.prepare(sql);
|
|
35
|
+
// better-sqlite3 expects individual parameters, not an array
|
|
36
|
+
if (params.length === 0) {
|
|
37
|
+
return (stmt.get() as T) || null;
|
|
38
|
+
}
|
|
39
|
+
return (stmt.get(...params) as T) || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Insert a record
|
|
44
|
+
*/
|
|
45
|
+
insert(table: string, data: Record<string, any>): { lastInsertRowid: number } {
|
|
46
|
+
const keys = Object.keys(data);
|
|
47
|
+
const values = Object.values(data);
|
|
48
|
+
const placeholders = keys.map(() => '?').join(', ');
|
|
49
|
+
|
|
50
|
+
const sql = `INSERT INTO ${table} (${keys.join(', ')}) VALUES (${placeholders})`;
|
|
51
|
+
const stmt = this.db.prepare(sql);
|
|
52
|
+
return stmt.run(...values) as any;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Update a record
|
|
57
|
+
*/
|
|
58
|
+
update(table: string, data: Record<string, any>, where: Record<string, any>): { changes: number } {
|
|
59
|
+
const setClause = Object.keys(data).map(k => `${k} = ?`).join(', ');
|
|
60
|
+
const whereClause = Object.keys(where).map(k => `${k} = ?`).join(' AND ');
|
|
61
|
+
|
|
62
|
+
const sql = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`;
|
|
63
|
+
const stmt = this.db.prepare(sql);
|
|
64
|
+
return stmt.run(...Object.values(data), ...Object.values(where)) as any;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Delete a record
|
|
69
|
+
*/
|
|
70
|
+
delete(table: string, where: Record<string, any>): { changes: number } {
|
|
71
|
+
const whereClause = Object.keys(where).map(k => `${k} = ?`).join(' AND ');
|
|
72
|
+
|
|
73
|
+
const sql = `DELETE FROM ${table} WHERE ${whereClause}`;
|
|
74
|
+
const stmt = this.db.prepare(sql);
|
|
75
|
+
return stmt.run(...Object.values(where)) as any;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check database connectivity
|
|
80
|
+
*/
|
|
81
|
+
async ping(): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
this.db.prepare('SELECT 1').get();
|
|
84
|
+
return true;
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { withToolData } from 'nitrostack/widgets';
|
|
4
|
+
import * as styles from '../../styles/ecommerce';
|
|
5
|
+
|
|
6
|
+
interface AddToCartData {
|
|
7
|
+
message: string;
|
|
8
|
+
product_id?: string;
|
|
9
|
+
product_name?: string;
|
|
10
|
+
price?: number;
|
|
11
|
+
quantity?: number;
|
|
12
|
+
image_url?: string;
|
|
13
|
+
cartItemCount?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function AddToCartWidget({ data }: { data: AddToCartData }) {
|
|
17
|
+
const { message, product_name, price, quantity, image_url, cartItemCount } = data;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style={styles.containerStyle}>
|
|
21
|
+
<div style={{
|
|
22
|
+
...styles.cardStyle,
|
|
23
|
+
maxWidth: '500px',
|
|
24
|
+
margin: '0 auto',
|
|
25
|
+
}}>
|
|
26
|
+
<div style={{
|
|
27
|
+
textAlign: 'center',
|
|
28
|
+
marginBottom: styles.spacing.xl,
|
|
29
|
+
}}>
|
|
30
|
+
<div style={{
|
|
31
|
+
width: '60px',
|
|
32
|
+
height: '60px',
|
|
33
|
+
margin: '0 auto 16px',
|
|
34
|
+
borderRadius: styles.borderRadius.full,
|
|
35
|
+
background: styles.colors.success,
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
fontSize: '28px',
|
|
40
|
+
}}>
|
|
41
|
+
✓
|
|
42
|
+
</div>
|
|
43
|
+
<h2 style={{
|
|
44
|
+
fontSize: styles.typography.fontSize.xl,
|
|
45
|
+
fontWeight: styles.typography.fontWeight.bold,
|
|
46
|
+
color: styles.colors.gray[900],
|
|
47
|
+
marginBottom: styles.spacing.sm,
|
|
48
|
+
}}>
|
|
49
|
+
{message}
|
|
50
|
+
</h2>
|
|
51
|
+
<p style={{
|
|
52
|
+
fontSize: styles.typography.fontSize.base,
|
|
53
|
+
color: styles.colors.gray[600],
|
|
54
|
+
}}>
|
|
55
|
+
You now have {cartItemCount || 1} {cartItemCount === 1 ? 'item' : 'items'} in your cart
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{product_name && (
|
|
60
|
+
<div style={{
|
|
61
|
+
display: 'flex',
|
|
62
|
+
gap: styles.spacing.md,
|
|
63
|
+
padding: styles.spacing.lg,
|
|
64
|
+
background: styles.colors.gray[50],
|
|
65
|
+
borderRadius: styles.borderRadius.md,
|
|
66
|
+
}}>
|
|
67
|
+
{image_url && (
|
|
68
|
+
<div style={{
|
|
69
|
+
width: '80px',
|
|
70
|
+
height: '80px',
|
|
71
|
+
flexShrink: 0,
|
|
72
|
+
borderRadius: styles.borderRadius.md,
|
|
73
|
+
overflow: 'hidden',
|
|
74
|
+
background: styles.colors.white,
|
|
75
|
+
}}>
|
|
76
|
+
<img
|
|
77
|
+
src={image_url}
|
|
78
|
+
alt={product_name}
|
|
79
|
+
style={{
|
|
80
|
+
width: '100%',
|
|
81
|
+
height: '100%',
|
|
82
|
+
objectFit: 'cover',
|
|
83
|
+
}}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<div style={{ flex: 1 }}>
|
|
89
|
+
<div style={{
|
|
90
|
+
fontSize: styles.typography.fontSize.base,
|
|
91
|
+
fontWeight: styles.typography.fontWeight.semibold,
|
|
92
|
+
color: styles.colors.gray[900],
|
|
93
|
+
marginBottom: styles.spacing.xs,
|
|
94
|
+
}}>
|
|
95
|
+
{product_name}
|
|
96
|
+
</div>
|
|
97
|
+
<div style={{
|
|
98
|
+
display: 'flex',
|
|
99
|
+
alignItems: 'center',
|
|
100
|
+
gap: styles.spacing.md,
|
|
101
|
+
}}>
|
|
102
|
+
{price && (
|
|
103
|
+
<div style={styles.priceStyle}>
|
|
104
|
+
${price.toFixed(2)}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
{quantity && (
|
|
108
|
+
<div style={styles.badgeStyle}>
|
|
109
|
+
Qty: {quantity}
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export default withToolData<AddToCartData>(AddToCartWidget);
|
|
122
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { withToolData } from 'nitrostack/widgets';
|
|
4
|
+
import * as styles from '../../styles/ecommerce';
|
|
5
|
+
|
|
6
|
+
interface AddressAddedData {
|
|
7
|
+
message: string;
|
|
8
|
+
addressId: string;
|
|
9
|
+
isDefault?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function AddressAddedWidget({ data }: { data: AddressAddedData }) {
|
|
13
|
+
const { message, addressId, isDefault } = data;
|
|
14
|
+
|
|
15
|
+
if (!addressId) {
|
|
16
|
+
return (
|
|
17
|
+
<div style={styles.containerStyle}>
|
|
18
|
+
<div style={styles.emptyStateStyle}>
|
|
19
|
+
<p style={styles.metaTextStyle}>Loading...</p>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div style={styles.containerStyle}>
|
|
27
|
+
<div style={{
|
|
28
|
+
...styles.cardStyle,
|
|
29
|
+
maxWidth: '500px',
|
|
30
|
+
margin: '0 auto',
|
|
31
|
+
}}>
|
|
32
|
+
<div style={{
|
|
33
|
+
textAlign: 'center',
|
|
34
|
+
marginBottom: styles.spacing.xl,
|
|
35
|
+
}}>
|
|
36
|
+
<div style={{
|
|
37
|
+
width: '60px',
|
|
38
|
+
height: '60px',
|
|
39
|
+
margin: '0 auto 16px',
|
|
40
|
+
borderRadius: styles.borderRadius.full,
|
|
41
|
+
background: styles.colors.success,
|
|
42
|
+
display: 'flex',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
fontSize: '28px',
|
|
46
|
+
}}>
|
|
47
|
+
✓
|
|
48
|
+
</div>
|
|
49
|
+
<h2 style={{
|
|
50
|
+
fontSize: styles.typography.fontSize.xl,
|
|
51
|
+
fontWeight: styles.typography.fontWeight.bold,
|
|
52
|
+
color: styles.colors.gray[900],
|
|
53
|
+
marginBottom: styles.spacing.sm,
|
|
54
|
+
}}>
|
|
55
|
+
{message}
|
|
56
|
+
</h2>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div style={{
|
|
60
|
+
background: styles.colors.gray[50],
|
|
61
|
+
borderRadius: styles.borderRadius.md,
|
|
62
|
+
padding: styles.spacing.lg,
|
|
63
|
+
marginBottom: styles.spacing.md,
|
|
64
|
+
}}>
|
|
65
|
+
<div style={{
|
|
66
|
+
fontSize: styles.typography.fontSize.sm,
|
|
67
|
+
color: styles.colors.gray[500],
|
|
68
|
+
marginBottom: styles.spacing.xs,
|
|
69
|
+
textTransform: 'uppercase',
|
|
70
|
+
letterSpacing: '0.5px',
|
|
71
|
+
fontWeight: styles.typography.fontWeight.semibold,
|
|
72
|
+
}}>
|
|
73
|
+
Address Reference
|
|
74
|
+
</div>
|
|
75
|
+
<div style={{
|
|
76
|
+
fontSize: styles.typography.fontSize.sm,
|
|
77
|
+
fontWeight: styles.typography.fontWeight.medium,
|
|
78
|
+
color: styles.colors.gray[900],
|
|
79
|
+
fontFamily: 'monospace',
|
|
80
|
+
wordBreak: 'break-all',
|
|
81
|
+
}}>
|
|
82
|
+
{addressId}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{isDefault && (
|
|
87
|
+
<div style={{
|
|
88
|
+
textAlign: 'center',
|
|
89
|
+
padding: styles.spacing.md,
|
|
90
|
+
background: `linear-gradient(135deg, ${styles.colors.primary} 0%, ${styles.colors.primaryHover} 100%)`,
|
|
91
|
+
color: styles.colors.black,
|
|
92
|
+
borderRadius: styles.borderRadius.md,
|
|
93
|
+
fontWeight: styles.typography.fontWeight.bold,
|
|
94
|
+
fontSize: styles.typography.fontSize.sm,
|
|
95
|
+
}}>
|
|
96
|
+
⭐ Default Shipping Address
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<div style={{
|
|
101
|
+
marginTop: styles.spacing.md,
|
|
102
|
+
padding: styles.spacing.md,
|
|
103
|
+
background: `${styles.colors.success}15`,
|
|
104
|
+
borderRadius: styles.borderRadius.md,
|
|
105
|
+
fontSize: styles.typography.fontSize.sm,
|
|
106
|
+
color: styles.colors.gray[700],
|
|
107
|
+
textAlign: 'center',
|
|
108
|
+
}}>
|
|
109
|
+
📦 Ready for deliveries
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default withToolData<AddressAddedData>(AddressAddedWidget);
|