📍 ロケーションサービス
Location Serviceは、親子関係と祖先追跡を備えた階層的なロケーションを管理するための完全なREST APIを提供します。Nodeblocksの関数型コンポジションアプローチとMongoDB統合を使用して、組織構造、地理的階層、ロケーションベースのデータを処理するように設計されています。
🚀 クイックスタート
import express from 'express';
import { middlewares, services, drivers } from '@nodeblocks/backend-sdk';
const { nodeBlocksErrorMiddleware } = middlewares;
const { locationService } = services;
const { withMongo } = drivers;
const connectToDatabase = withMongo('mongodb://localhost:27017', 'dev', 'user', 'password');
express()
.use(
locationService(
{
...(await connectToDatabase('locations')),
...(await connectToDatabase('identities')),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
}
)
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
📋 エンドポイント概要
ロケーション操作
| Method | Path | 説明 | 認証が必要 |
|---|---|---|---|
POST | /locations | 新しいロケーションを作成 | ✅ 管理者 |
GET | /locations/:locationId | IDでロケーションを取得 | ❌ |
GET | /locations | すべてのロケーションをリスト | ❌ |
PATCH | /locations/:locationId | ロケーションを更新 | ✅ 管理者 |
DELETE | /locations/:locationId | ロケーションを削除 | ✅ 管理者 |
🗄️ エンティティスキーマ
ロケーションエンティティは、基本フィールド(自動生成)とロケーション固有のデータを組み合わせます:
{
"name": "string",
"code": "string",
"type": "string",
"parentId": "string",
"ancestors": ["string"],
"createdAt": "string (datetime)",
"id": "string",
"updatedAt": "string (datetime)"
}
フィールド詳細
| Field | Type | 自動生成 | 必須 | 説明 |
|---|---|---|---|---|
name | string | ❌ | ✅ | ロケーション名 |
code | string | ❌ | ✅ | 一意のロケーション識別子コード |
type | string | ❌ | ✅ | ロケーションタイプ分類(ORGANIZATION、REGION、CITY、BUILDING) |
parentId | string | ❌ | ❌ | 階層構造のための親ロケーションID |
ancestors | string[] | ✅ | ✅ | 祖先ロケーションIDの配列(自動計算) |
createdAt | datetime | ✅ | ✅ | 作成タイムスタンプ |
id | string | ✅ | ✅ | 一意の識別子(UUID) |
updatedAt | datetime | ✅ | ✅ | 最終更新タイムスタンプ |
📝 注記: 自動生成フィールドはサービスによって設定され、作成/更新リクエストに含めるべきではありません。
parentIdフィールドは階層的なロケーション構造を可能にし、ancestorsは親階層から自動的に計算されます。
🔐 認証ヘッダー
保護されたエンドポイントでは、次のヘッダーを含めてください:
Authorization: Bearer <admin_access_token>
x-nb-fingerprint: <device_fingerprint>
⚠️ 重要:
x-nb-fingerprintヘッダーは、認証時にフィンガープリントが指定された場合、すべての認証済みリクエストで必須です。これがない場合、リクエストは401 Unauthorizedを返します。
🔧 APIエンドポイント
1. ロケーションの作成
提供された情報とオプションの階層的な親関係で新しいロケーションを作成します。
リクエスト:
- Method:
POST - Path:
/locations - Headers:
Content-Type: application/jsonAuthorization: Bearer <token>x-nb-fingerprint: <device-fingerprint>
- Authorization: Bearerトークンが必要(管理者)
リクエストボディ:
| Field | Type | 必須 | 説明 |
|---|---|---|---|
name | string | ✅ | ロケーション名 |
code | string | ✅ | 一意のロケーション識別子コード |
type | string | ✅ | ロケーションタイプ分類 |
parentId | string | ❌ | 階層構造のための親ロケーションID |
レスポンスボディ:
| Field | Type | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | ロケーション名 |
code | string | 一意のロケーション識別子コード |
type | string | ロケーションタイプ分類 |
parentId | string | 親ロケーションID(該当する場合) |
ancestors | string[] | 祖先ロケーションIDの配列 |
createdAt | string | 作成タイムスタンプ |
updatedAt | string | 最終更新タイムスタンプ |
バリデーション:
- スキーマバリデーション: 自動的に強制されます(name、code、type必須)
- ルートバリデーター:
- 認証済みリクエストが必要(bearerトークン)
- 管理者ロールが必要
リクエスト例:
curl -X POST http://localhost:8089/locations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>" \
-d '{
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123"
}'
成功レスポンス:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": "location-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123",
"ancestors": ["org-456", "region-789", "city-123"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
2. ロケーションのリスト取得
公開アクセスとページネーションサポートを使用して、ページネーション付きのロケーションリストを取得します。
リクエスト:
- Method:
GET - Path:
/locations - Authorization: 不要
クエリパラメータ:
| Parameter | Type | 必須 | 説明 |
|---|---|---|---|
page | number | ❌ | ページネーションのページ番号(1-1000) |
limit | number | ❌ | ページあたりの項目数(1-50) |
レスポンスボディ: ロケーション配列とメタデータを含むページネーション付きレスポンス。
レスポンス構造:
{
"data": [
{
"id": "string",
"name": "string",
"code": "string",
"type": "string",
"parentId": "string",
"ancestors": ["string"],
"createdAt": "string",
"updatedAt": "string"
}
],
"metadata": {
"pagination": {
"page": number,
"limit": number,
"total": number,
"totalPages": number,
"hasNext": boolean,
"hasPrev": boolean
}
}
}
バリデーション:
- スキーマバリデーション: ページネーション(page、limit)のクエリパラメータバリデーション
- ルートバリデーター: なし
リクエスト例:
curl "http://localhost:8089/locations?page=1&limit=10"
成功レスポンス:
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{
"id": "location-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-123",
"ancestors": ["org-456", "region-789", "city-123"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
],
"metadata": {
"pagination": {
"page": 1,
"limit": 10,
"total": 25,
"totalPages": 3,
"hasNext": true,
"hasPrev": false
}
}
}
3. IDでロケーションを取得
公開アクセスでIDによってロケーションを取得します。
リクエスト:
- Method:
GET - Path:
/locations/:locationId - Authorization: 不要
URLパラメータ:
| Parameter | Type | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
レスポンスボディ:
| Field | Type | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | ロケーション名 |
code | string | 一意のロケーション識別子コード |
type | string | ロケーションタイプ分類 |
parentId | string | 親ロケーションID(該当する場合) |
ancestors | string[] | 祖先ロケーションIDの配列 |
createdAt | string | 作成タイムスタンプ |
updatedAt | string | 最終更新タイムスタンプ |
バリデーション:
- スキーマバリデーション: locationIdのパスパラメータバリデーション
- ルートバリデーター: なし
リクエスト例:
curl http://localhost:8089/locations/loc-123
成功レスポンス:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "loc-123",
"name": "San Francisco",
"code": "SF",
"type": "CITY",
"parentId": "region-456",
"ancestors": ["org-789", "region-456"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
4. ロケーションの更新
部分的なフィールド変更と管理者認証で既存のロケーションを更新します。
リクエスト:
- Method:
PATCH - Path:
/locations/:locationId - Headers:
Content-Type: application/json - Authorization: 必要(管理者)
URLパラメータ:
| Parameter | Type | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
リクエストボディ(すべてのフィールドはオプション):
| Field | Type | 必須 | 説明 |
|---|---|---|---|
name | string | ❌ | ロケーション名 |
code | string | ❌ | 一意のロケーション識別子コード |
type | string | ❌ | ロケーションタイプ分類 |
レスポンスボディ:
| Field | Type | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | 更新されたロケーション名 |
code | string | 更新されたロケーション識別子コード |
type | string | 更新されたロケーションタイプ |
parentId | string | 親ロケーションID(変更なし) |
ancestors | string[] | 祖先ロケーションIDの配列(変更なし) |
createdAt | string | 作成タイムスタンプ |
updatedAt | string | 最終更新タイムスタンプ |
バリデーション:
- スキーマバリデーション: 自動的に強制されます(部分更新、限られたフィールドのみ許可)
- ルートバリデーター:
- 認証済みリクエストが必要(bearerトークン)
- 管理者ロールが必要
リクエスト例:
curl -X PATCH http://localhost:8089/locations/loc-123 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>" \
-d '{
"name": "Updated Headquarters Name",
"code": "HQ-UPDATED"
}'
成功レスポンス:
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "loc-123",
"name": "Updated Headquarters Name",
"code": "HQ-UPDATED",
"type": "BUILDING",
"parentId": "region-456",
"ancestors": ["org-789", "region-456"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-16T14:20:00Z"
}
5. ロケーションの削除
孤立した子ロケーションを防ぐために、包括的な階層検証で既存のロケーションを削除します。
リクエスト:
- Method:
DELETE - Path:
/locations/:locationId - Authorization: 必要(管理者)
URLパラメータ:
| Parameter | Type | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
レスポンスボディ:
| Field | Type | 説明 |
|---|---|---|
| レスポンスボディなし | - | 削除エンドポイントは成功時にレスポンスボディを返しません |
バリデーション:
- スキーマバリデーション: locationIdのパスパラメータバリデーション
- ルートバリデーター:
- 認証済みリクエストが必要(bearerトークン)
- 管理者ロールが必要
- 子孫ロケーションが存在しないことを検証(子が存在する場合、削除を防止)
リクエスト例:
curl -X DELETE http://localhost:8089/locations/loc-123 \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>"
成功レスポンス:
HTTP/1.1 204 No Content
エラーレスポンス:
ロケーションに子孫ロケーションがある場合:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"message": "Location has descendant locations"
}
}
ロケーションが見つからない場合:
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"message": "Location not found"
}
}
ロケーションタイプ
サービスはさまざまなロケーションタイプ分類をサポートしています:
| Type | 説明 | 例 |
|---|---|---|
ORGANIZATION | ルートレベルの組織エンティティ | Global Corp、Acme Inc |
REGION | 地理的または管理的地域 | West Coast、EMEA、APAC |
CITY | 市または都市部 | New York、Tokyo、London |
BUILDING | 物理的な構造物または施設 | HQ Building、Branch Office |
階層関係
親子関係
ロケーションは親子関係を持つことができます:
// Create organization (no parent)
const org = await createLocation({
name: 'Tech Corp',
code: 'TECH',
type: 'ORGANIZATION'
});
// Create region under organization
const region = await createLocation({
name: 'West Coast',
code: 'WEST',
type: 'REGION',
parentId: org.id
});
// Create city under region
const city = await createLocation({
name: 'San Francisco',
code: 'SF',
type: 'CITY',
parentId: region.id
});
祖先チェーン
各ロケーションは、効率的な階層クエリのために完全な祖先チェーンを維持します:
{
"id": "building-uuid",
"name": "Headquarters",
"code": "HQ",
"type": "BUILDING",
"parentId": "city-uuid",
"ancestors": ["org-uuid", "region-uuid", "city-uuid"],
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:00Z"
}
認証とセキュリティ
管理者専用アクセス
現在、すべてのロケーション操作には管理者権限が必要です:
// Service configuration with admin identity type
const config = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret'
},
identity: {
typeIds: {
admin: '100', // Admin users
guest: '000', // Guest users
regular: '001' // Regular users
}
}
};
今後の認証強化
計画されている機能:
- 組織ベースのアクセス制御
- ロケーションタイプ固有の権限
- メール確認要件
- 階層的な権限継承
使用例
基本設定
import express from 'express';
import { locationService } from '@nodeblocks/backend-sdk';
import { withMongo } from '@nodeblocks/backend-sdk/drivers';
const app = express();
// Database setup
const connectToDatabase = withMongo('mongodb://localhost:27017', 'dev', 'user', 'password');
// Service configuration
const locationServiceConfig = {
authSecrets: {
authEncSecret: process.env.AUTH_ENC_SECRET,
authSignSecret: process.env.AUTH_SIGN_SECRET
},
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001'
}
}
};
// Data stores
const dataStores = {
...(await connectToDatabase('identities')),
...(await connectToDatabase('locations'))
};
// Mount service
app.use('/api/locations', locationService(dataStores, locationServiceConfig));
ロケーション階層の作成
// 1. Create root organization
const orgResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Global Enterprises',
code: 'GLOBAL',
type: 'ORGANIZATION'
})
});
// 2. Create regional division
const regionResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'North America',
code: 'NA',
type: 'REGION',
parentId: orgResponse.body.id
})
});
// 3. Create city location
const cityResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'New York',
code: 'NYC',
type: 'CITY',
parentId: regionResponse.body.id
})
});
// 4. Create building
const buildingResponse = await fetch('/api/locations', {
method: 'POST',
headers: {
'Authorization': `Bearer ${adminToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Manhattan Office',
code: 'MANHATTAN',
type: 'BUILDING',
parentId: cityResponse.body.id
})
});
console.log('Created hierarchy:');
console.log('Organization -> Region -> City -> Building');
サービスアーキテクチャ
機能コンポジション
ロケーションサービスはモジュール性のために機能コンポジションを使用します:
export const locationService: LocationService = (dataStores, configuration) => {
return defService(
partial(compose(createLocationFeature), [{ configuration, dataStores }])
);
};
ミドルウェア統合
サービスは標準のNodeblocksミドルウェアと統合します:
- 認証: Bearerトークン検証
- 認可: アイデンティティタイプチェック
- エラーハンドリング: 構造化されたエラーレスポンス
- リクエストロギング: 包括的なリクエスト追跡
データベースの考慮事項
推奨インデックス
最適なパフォーマンスのために、locationsコレクションにこれらのインデックスを作成してください:
// Unique index on location ID
db.locations.createIndex({ id: 1 }, { unique: true });
// Index for parent-child queries
db.locations.createIndex({ parentId: 1 });
// Index for ancestor chain queries
db.locations.createIndex({ ancestors: 1 });
// Compound index for hierarchical queries
db.locations.createIndex({ type: 1, ancestors: 1 });
// Index for code lookups
db.locations.createIndex({ code: 1 }, { unique: true });
データ整合性
サービスは以下を通じてデータ整合性を確保します:
- アトミック操作: ロケーション作成のための単一トランザクション
- 参照整合性: 作成前の親検証
- 祖先の正確性: 自動祖先チェーン計算
- 一意制約: コードの一意性強制
エラーハンドリング
一般的なエラーレスポンス
バリデーションエラー(400)
{
"error": {
"data": [
"request body must have required property 'name'",
"request body must have required property 'code'"
],
"message": "Validation Error"
}
}
認証エラー(403)
{
"error": {
"message": "Forbidden"
}
}
見つからないエラー(404)
{
"error": {
"message": "Location not found"
}
}
データベースエラー(500)
{
"error": {
"message": "Failed to create location"
}
}
今後の強化
計画されている機能
- ロケーション取得: ロケーションクエリ用のGETエンドポイント
- ロケーション更新: ロケーション変更用のPATCHエンドポイント
- ロケーション削除: カスケードオプション付きDELETEエンドポイント
- 一括操作: 一括作成、更新、削除
- 高度なクエリ: タイプ、階層、カスタム基準によるフィルタリング
- 地理的特徴: 座標サポートと地理的クエリ
- インポート/エクスポート: CSVおよびJSONインポート/エクスポート機能
拡張認証
- ロールベースアクセス: ロケーション固有の権限
- 組織スコープ: 組織ベースのロケーションアクセス
- 階層的権限: ロケーションツリーを通じた権限継承
移行とセットアップ
データベースセットアップ
// Create locations collection
db.createCollection('locations');
// Create indexes
db.locations.createIndex({ id: 1 }, { unique: true });
db.locations.createIndex({ parentId: 1 });
db.locations.createIndex({ ancestors: 1 });
db.locations.createIndex({ type: 1, ancestors: 1 });
db.locations.createIndex({ code: 1 }, { unique: true });
アプリケーション統合
// Add to main application
import { locationService } from '@nodeblocks/backend-sdk';
// Configure and mount
const locationConfig = { /* configuration */ };
const locationDataStores = { /* data stores */ };
app.use('/api/locations', locationService(locationDataStores, locationConfig));
テスト
サービステスト
import { locationService } from '@nodeblocks/backend-sdk';
import request from 'supertest';
// Test service integration
describe('Location Service', () => {
it('Should create locations with proper hierarchy', async () => {
const app = express();
app.use('/locations', locationService(dataStores, config));
const response = await request(app)
.post('/locations')
.set('Authorization', `Bearer ${adminToken}`)
.send({
name: 'Test Location',
code: 'TEST',
type: 'BUILDING'
});
expect(response.status).toBe(201);
expect(response.body.ancestors).toEqual([]);
});
});
Location Serviceは、将来の拡張と機能強化の余地がある階層的なロケーション管理のための堅牢な基盤を提供します。