📍 ロケーションサービス
ロケーションサービスは、親子関係と祖先追跡を持つ階層的なロケーションを管理する完全な REST API を提供します。Nodeblocks の関数型コンポジションアプローチと MongoDB 統合を利用し、組織構造、地理的な階層、ロケーションベースのデータを扱うために設計されています。
🚀 Quickstart
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/?authSource=admin', 'dev', 'user', 'password');
express()
.use(
locationService(
{
...(await connectToDatabase('locations')),
...(await connectToDatabase('identities')),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer', // または 'cookie'
identity: {
typeIds: {
admin: '100',
guest: '000',
regular: '001',
},
},
},
),
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
📋 Endpoint Summary
ロケーション操作
| メソッド | パス | 説明 | 認証必要 |
|---|---|---|---|
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)"
}
フィールド詳細
| フィールド | 型 | 自動生成 | 必須 | 説明 |
|---|---|---|---|---|
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は親階層から自動計算されます。
🔐 認証ヘッダー
保護されたエンドポイントでは、authMode が 'bearer' の場合は Authorization ヘッダーでアクセストークンを指定し、'cookie' の場合は Cookie からトークンを読み取ります。
Authorization: Bearer <admin_access_token>
x-nb-fingerprint: <device_fingerprint>
⚠️ 重要: 認可時にフィンガープリントを指定した場合、すべての認証済みリクエストで
x-nb-fingerprintヘッダーが必須です。指定しないリクエストは 401 Unauthorized を返します。
🔧 API エンドポイント
1. ロケーションの作成
指定した情報と、任意の階層的な親子関係で新しいロケーションを作成します。
リクエスト:
- メソッド:
POST - パス:
/locations - ヘッダー:
Content-Type: application/jsonAuthorization: Bearer <token>x-nb-fingerprint: <device-fingerprint>
- 認可: アクセストークン必須(既定は
bearer、authMode: 'cookie'の場合はcookie)。管理者のみ
リクエスト本文:
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | ✅ | ロケーション名 |
code | string | ✅ | 一意のロケーション識別コード |
type | string | ✅ | ロケーション種別 |
parentId | string | ❌ | 階層構造の親ロケーション ID |
レスポンス本文:
| フィールド | 型 | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | ロケーション名 |
code | string | 一意のロケーション識別コード |
type | string | ロケーション種別 |
parentId | string | 親ロケーション ID(該当する場合) |
ancestors | string[] | 祖先ロケーション ID の配列 |
createdAt | string | 作成日時 |
updatedAt | string | 最終更新日時 |
検証:
- スキーマ検証: 自動適用(
name、code、typeが必須) - ルートバリデーター:
- 認証済みリクエスト(アクセストークン)が必要
- 管理者ロールが必要
リクエスト例:
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. ロケーション一覧
公開アクセスで、ページネーション対応のロケーション一覧を取得します。
リクエスト:
- メソッド:
GET - パス:
/locations - 認可: 不要
クエリパラメーター:
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
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
}
}
}
Validation:
- スキーマ検証: ページネーション(
page、limit)のクエリパラメーターを検証 - ルートバリデーター: なし
Example Request:
curl "http://localhost:8089/locations?page=1&limit=10"
Success Response:
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 によってロケーションを取得します。
リクエスト:
- メソッド:
GET - パス:
/locations/:locationId - 認可: 不要
URL パラメーター:
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
レスポンス本文:
| フィールド | 型 | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | ロケーション名 |
code | string | 一意のロケーション識別コード |
type | string | ロケーション種別 |
parentId | string | 親ロケーション ID(該当する場合) |
ancestors | string[] | 祖先ロケーション ID の配列 |
createdAt | string | 作成日時 |
updatedAt | string | 最終更新日時 |
Validation:
- スキーマ検証:
locationIdパスパラメーターを検証 - ルートバリデーター: なし
Example Request:
curl http://localhost:8089/locations/loc-123
Success Response:
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. ロケーションの更新
管理者認証のもと、既存ロケーションのフィールドを部分更新します。
リクエスト:
- メソッド:
PATCH - パス:
/locations/:locationId - ヘッダー:
Content-Type: application/json - 認可: アクセストークン必須(既定は
bearer、authMode: 'cookie'の場合はcookie)。管理者のみ
URL パラメーター:
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
リクエスト本文(すべて任意):
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
name | string | ❌ | ロケーション名 |
code | string | ❌ | 一意のロケーション識別コード |
type | string | ❌ | ロケーション種別 |
レスポンス本文:
| フィールド | 型 | 説明 |
|---|---|---|
id | string | 一意のロケーション識別子 |
name | string | 更新後のロケーション名 |
code | string | 更新後のロケーション識別コード |
type | string | 更新後のロケーション種別 |
parentId | string | 親ロケーション ID(変更されません) |
ancestors | string[] | 祖先ロケーション ID の配列(変更されません) |
createdAt | string | 作成日時 |
updatedAt | string | 最終更新日時 |
Validation:
- スキーマ検証: 自動適用(部分更新。指定できるフィールドは限定されます)
- ルートバリデーター:
- 認証済みリクエスト(アクセストークン)が必要
- 管理者ロールが必要
Example Request:
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"
}'
Success Response:
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. ロケーションの削除
子ロケーションが孤立しないよう包括的な階層検証を行い、既存のロケーションを削除します。
リクエスト:
- メソッド:
DELETE - パス:
/locations/:locationId - 認可: アクセストークン必須(既定は
bearer、authMode: 'cookie'の場合はcookie)。管理者のみ
URL パラメーター:
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
locationId | string | ✅ | 一意のロケーション識別子 |
レスポンス本文:
| フィールド | 型 | 説明 |
|---|---|---|
| レスポンスボディなし | - | 削除エンドポイントは成功時にレスポンスボディを返しません |
Validation:
- スキーマ検証:
locationIdパスパラメーターを検証 - ルートバリデーター:
- 認証済みリクエスト(アクセストークン)が必要
- 管理者ロールが必要
- 子ロケーションが存在しないことを検証(子がある場合は削除を防止)
Example Request:
curl -X DELETE http://localhost:8089/locations/loc-123 \
-H "Authorization: Bearer <admin_token>" \
-H "x-nb-fingerprint: <device-fingerprint>"
Success Response:
HTTP/1.1 204 No Content
エラーレスポンス:
ロケーションに子孫ロケーションが存在する場合:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": {
"message": "Dependent child locations still exist"
}
}
ロケーションを削除できず、削除処理に失敗した場合:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": {
"message": "Location could not be deleted"
}
}
ロケーション種別
SDK は
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"
}
認可とセキュリティ
アクセス制御
ロケーションエンドポイントでは、操作ごとに異なる認可ルールを使用します。
| メソッド | パス | 認可 |
|---|---|---|
GET | /locations | 公開(認証不要) |
GET | /locations/:locationId | 公開(認証不要) |
POST | /locations | 認証済み管理者 |
PATCH | /locations/:locationId | 認証済み管理者 |
DELETE | /locations/:locationId | 認証済み管理者 |
// Service configuration with admin identity type (required for write operations)
const config = {
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer', // or 'cookie'
identity: {
typeIds: {
admin: '100', // Admin users
guest: '000', // Guest users
regular: '001', // Regular users
},
},
};
⚙️ 設定オプション
サービス設定
interface LocationServiceConfiguration {
authSecrets: {
authEncSecret: string; // JWT encryption secret
authSignSecret: string; // JWT signing secret
};
authMode?: 'bearer' | 'cookie'; // 未指定時はベアラー認証
identity?: {
typeIds?: {
admin: string;
guest: string;
regular: string;
};
};
}
🍪 Cookie 認証:
authMode: 'cookie'の場合、保護されたルートは Cookie からアクセストークンを読み取ります。ホストアプリでcookie-parserを登録してください。
データストア
| コレクション | 必須 | 説明 |
|---|---|---|
identities | ✅ | アイデンティティ検索/認証コンテキスト |
locations | ✅ | ロケーション文書 |
使用例
基本セットアップ
import express from 'express';
import {services, drivers} from '@nodeblocks/backend-sdk';
const {locationService} = services;
const {withMongo} = drivers;
const app = express();
// Database setup
const connectToDatabase = withMongo('mongodb://localhost:27017/?authSource=admin', 'dev', 'user', 'password');
// Service configuration
const locationServiceConfig = {
authSecrets: {
authEncSecret: process.env.AUTH_ENC_SECRET,
authSignSecret: process.env.AUTH_SIGN_SECRET,
},
authMode: 'bearer', // or 'cookie'
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,
getLocationFeature,
updateLocationFeature,
deleteLocationFeature,
findLocationsFeature,
),
[
{
authenticate: configuration.authMode === 'cookie' ? getCookieTokenInfo : getBearerTokenInfo,
configuration,
dataStores,
},
],
),
);
};
ミドルウェア統合
このサービスは標準の Nodeblocks ミドルウェアと統合されます。
- 認証: ベアラートークンの検証
- 認可: アイデンティティ種別の確認
- エラー処理: 構造化されたエラーレスポンス
- リクエストログ: 包括的なリクエスト追跡
データベースに関する考慮事項
推奨インデックス
最適なパフォーマンスのため、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});
データ整合性
このサービスは次の方法で整合性を維持します。
- 参照整合性: 作成前に親を検証
- 祖先の正確性: 祖先チェーンを自動計算
- コードの一意性: 検索時の整合性確保のため
codeに一意インデックスを推奨
エラーハンドリング
一般的なエラーレスポンス
検証エラー(400)
{
"error": {
"data": [
"request body must have required property 'name'",
"request body must have required property 'code'",
"request body must have required property 'type'"
],
"message": "Validation Error"
}
}
認可エラー(403)
{
"error": {
"message": "Identity is not authorized to access this resource"
}
}
未検出エラー(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 {services} from '@nodeblocks/backend-sdk';
const {locationService} = services;
// Configure and mount
const locationConfig = {
/* configuration */
};
const locationDataStores = {
/* data stores */
};
app.use('/api/locations', locationService(locationDataStores, locationConfig));
テスト
サービステスト
import {services} from '@nodeblocks/backend-sdk';
const {locationService} = services;
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([]);
});
});
ロケーションサービスは、将来の拡張や機能強化の余地を備えた、階層的ロケーション管理の堅牢な基盤を提供します。