🛍️ 商品サービス
商品サービスは、バッチ操作とコピー機能を含む商品エンティティ管理のための完全なREST APIを提供します。Nodeblocks関数コンポジションアプローチを使用して構築され、MongoDBとシームレスに統合されます。
🚀 クイックスタート
import express from 'express';
import { MongoClient } from 'mongodb';
import { middlewares, services } from '@nodeblocks/backend-sdk';
const { nodeBlocksErrorMiddleware } = middlewares;
const { productService } = services;
const client = new MongoClient('mongodb://localhost:27017').db('dev');
express()
.use(
productService(
{
products: client.collection('products'),
identity: client.collection('identity'),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
user: {
typeIds: {
admin: '100',
guest: '000',
user: '001',
},
},
}
)
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('商品サービスが起動しました'));
📋 エンドポイント概要
メソッド | パス | 説明 | 認証 |
---|---|---|---|
POST | /products | 新規商品を作成 | Bearerトークン必須 |
GET | /products/:productId | IDで商品を取得 | 公開アクセス |
GET | /products | 商品リストを取得 | 公開アクセス |
PATCH | /products/:productId | 商品を更新 | Bearerトークン必須(管理者) |
DELETE | /products/:productId | 商品を削除 | Bearerトークン必須(管理者) |
POST | /products/:productId/copy | 商品をコピー | Bearerトークン必須(管理者) |
POST | /products/batch | バッチ操作を実行 | Bearerトークン必須(管理者) |
🔧 必要な設定
データストア
商品サービスには以下のMongoDBコレクションが必要です:
const dataStores = {
products: client.collection('products'), // 商品データ
identity: client.collection('identity'), // 認証情報
};
設定オプション
interface ProductConfig {
authSecrets: {
authEncSecret: string;
authSignSecret: string;
};
user: {
typeIds: {
admin: string; // 管理者タイプID
guest: string; // ゲストタイプID
user: string; // 一般ユーザータイプID
};
};
}
🛍️ 商品操作
1. 商品作成
curl -X POST http://localhost:8089/products \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "サンプル商品",
"description": "この商品の詳細説明",
"price": 1299.99,
"category": "electronics",
"sku": "PROD-001",
"inventory": {
"quantity": 100,
"threshold": 10
},
"specifications": {
"color": "ブラック",
"weight": "1.5kg",
"dimensions": "30x20x15cm"
}
}'
レスポンス:
{
"productId": "uuid-here",
"message": "商品が正常に作成されました"
}
2. 商品取得
# 特定商品の取得
curl -X GET http://localhost:8089/products/product-uuid-here
# 商品リストの取得
curl -X GET http://localhost:8089/products
# フィルタ付き検索
curl -X GET "http://localhost:8089/products?category=electronics&price_min=100&price_max=2000"
レスポンス例:
{
"id": "uuid-here",
"name": "サンプル商品",
"description": "この商品の詳細説明",
"price": 1299.99,
"category": "electronics",
"sku": "PROD-001",
"status": "active",
"inventory": {
"quantity": 95,
"threshold": 10,
"status": "in_stock"
},
"specifications": {
"color": "ブラック",
"weight": "1.5kg",
"dimensions": "30x20x15cm"
},
"images": [
"https://example.com/product-image-1.jpg"
],
"createdAt": "2024-01-15T10:30:00.000Z",
"updatedAt": "2024-01-15T10:30:00.000Z"
}
3. 商品更新
curl -X PATCH http://localhost:8089/products/product-uuid-here \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"price": 1199.99,
"inventory": {
"quantity": 85
}
}'
📦 高度な機能
商品コピー
curl -X POST http://localhost:8089/products/product-uuid-here/copy \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "コピーされた商品",
"sku": "PROD-001-COPY",
"modifications": {
"price": 999.99,
"specifications": {
"color": "ホワイト"
}
}
}'
バッチ操作
# 複数商品の一括更新
curl -X POST http://localhost:8089/products/batch \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": "update",
"products": [
{
"id": "product-1-uuid",
"data": { "status": "discontinued" }
},
{
"id": "product-2-uuid",
"data": { "price": 899.99 }
}
]
}'
# 複数商品の一括削除
curl -X POST http://localhost:8089/products/batch \
-H "Authorization: Bearer ADMIN_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operation": "delete",
"productIds": [
"product-1-uuid",
"product-2-uuid",
"product-3-uuid"
]
}'
📊 データスキーマ
商品エンティティ
interface Product {
id: string; // 一意識別子
name: string; // 商品名(必須)
description?: string; // 商品説明
price: number; // 価格(必須)
category?: string; // カテゴリ
sku?: string; // 商品コード(一意)
status?: 'draft' | 'active' | 'inactive' | 'discontinued';
inventory?: {
quantity: number; // 在庫数
threshold: number; // 在庫警告しきい値
status: 'in_stock' | 'low_stock' | 'out_of_stock';
};
specifications?: Record<string, any>; // 商品仕様
images?: string[]; // 画像URL配列
tags?: string[]; // タグ
metadata?: Record<string, any>; // 追加メタデータ
createdAt: string; // 作成日時
updatedAt: string; // 更新日時
}
検証ルール
const productValidation = {
name: {
required: true,
minLength: 2,
maxLength: 200
},
price: {
required: true,
type: 'number',
minimum: 0
},
sku: {
unique: true,
pattern: '^[A-Z0-9-]+$',
optional: true
},
category: {
enum: ['electronics', 'clothing', 'books', 'home', 'sports', 'other']
},
status: {
enum: ['draft', 'active', 'inactive', 'discontinued'],
default: 'draft'
}
};
🔍 検索とフィルタリング
高度な検索クエリ
# テキスト検索
curl -X GET "http://localhost:8089/products?search=ノートパソコン"
# 価格範囲フィルタ
curl -X GET "http://localhost:8089/products?price_min=500&price_max=1500"
# カテゴリフィルタ
curl -X GET "http://localhost:8089/products?category=electronics"
# ステータスフィルタ
curl -X GET "http://localhost:8089/products?status=active"
# 在庫状態フィルタ
curl -X GET "http://localhost:8089/products?inventory_status=in_stock"
# 並び替え
curl -X GET "http://localhost:8089/products?sort=price&order=asc"
# ページネーション
curl -X GET "http://localhost:8089/products?page=1&limit=20"
# 複合クエリ
curl -X GET "http://localhost:8089/products?category=electronics&price_min=100&sort=created_at&order=desc&page=1&limit=10"
検索レスポンス
{
"products": [...],
"pagination": {
"page": 1,
"limit": 10,
"total": 150,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
},
"filters": {
"category": "electronics",
"priceRange": { "min": 100, "max": null },
"status": "active"
}
}
🎯 高度な使用方法
カスタム商品属性
// 拡張商品スキーマ
const extendedProductConfig = {
...baseConfig,
customFields: {
brand: { type: 'string', required: true },
model: { type: 'string' },
warranty: {
duration: { type: 'number' }, // 月単位
type: { type: 'string', enum: ['limited', 'extended', 'lifetime'] }
},
shipping: {
weight: { type: 'number' },
dimensions: {
length: { type: 'number' },
width: { type: 'number' },
height: { type: 'number' }
},
shippingClass: { type: 'string', enum: ['standard', 'express', 'overnight'] }
}
}
};
在庫管理
// 在庫更新
const updateInventory = async (productId, quantityChange, operation = 'subtract') => {
const product = await getProduct(productId);
const currentQuantity = product.inventory.quantity;
let newQuantity;
if (operation === 'add') {
newQuantity = currentQuantity + quantityChange;
} else {
newQuantity = Math.max(0, currentQuantity - quantityChange);
}
// 在庫状態を自動計算
let inventoryStatus = 'in_stock';
if (newQuantity === 0) {
inventoryStatus = 'out_of_stock';
} else if (newQuantity <= product.inventory.threshold) {
inventoryStatus = 'low_stock';
}
return await updateProduct(productId, {
inventory: {
...product.inventory,
quantity: newQuantity,
status: inventoryStatus
}
});
};
価格管理
// 価格変更履歴
const priceHistory = {
trackPriceChange: async (productId, oldPrice, newPrice, reason) => {
const priceChange = {
productId,
oldPrice,
newPrice,
change: ((newPrice - oldPrice) / oldPrice * 100).toFixed(2),
reason,
changedAt: new Date().toISOString(),
changedBy: getCurrentUser().id
};
await savePriceHistory(priceChange);
},
// 自動価格調整
applyDiscount: async (productIds, discountPercent, startDate, endDate) => {
const updates = productIds.map(async (productId) => {
const product = await getProduct(productId);
const discountedPrice = product.price * (1 - discountPercent / 100);
return updateProduct(productId, {
price: discountedPrice,
originalPrice: product.price,
discount: {
percent: discountPercent,
startDate,
endDate
}
});
});
return await Promise.all(updates);
}
};
🧪 テスト
ユニットテスト例
import { productService } from '@nodeblocks/backend-sdk';
import { createMemoryDataStore } from '../test-utils';
describe('商品サービス', () => {
let productApp;
let testDataStore;
let adminToken;
beforeEach(async () => {
testDataStore = createMemoryDataStore();
productApp = productService(
{
products: testDataStore.collection('products'),
identity: testDataStore.collection('identity'),
},
testConfig
);
adminToken = await createTestToken('admin');
});
test('商品作成が成功する', async () => {
const productData = {
name: 'テスト商品',
description: 'テスト用商品',
price: 999.99,
category: 'electronics',
sku: 'TEST-001'
};
const response = await request(productApp)
.post('/products')
.set('Authorization', `Bearer ${adminToken}`)
.send(productData)
.expect(201);
expect(response.body.message).toBe('商品が正常に作成されました');
});
test('重複SKUを拒否する', async () => {
const productData = {
name: '商品1',
price: 100,
sku: 'DUPLICATE-SKU'
};
// 最初の商品を作成
await createProduct(productData);
// 同じSKUで2番目の商品作成を試行
await request(productApp)
.post('/products')
.set('Authorization', `Bearer ${adminToken}`)
.send({ ...productData, name: '商品2' })
.expect(400);
});
test('商品コピーが成功する', async () => {
const originalProduct = await createProduct({
name: '元の商品',
price: 100,
sku: 'ORIGINAL-001'
});
const copyData = {
name: 'コピーされた商品',
sku: 'COPY-001',
modifications: { price: 150 }
};
const response = await request(productApp)
.post(`/products/${originalProduct.id}/copy`)
.set('Authorization', `Bearer ${adminToken}`)
.send(copyData)
.expect(201);
expect(response.body.message).toBe('商品が正常にコピーされました');
});
});
🔗 関連リソース
➡️ 次のステップ
商品サービスを設定したら、カテゴリサービスと統合して商品分類機能を追加することを検討してください。