メインコンテンツまでスキップ
バージョン: 0.4.2

🎯 既存スキーマのオーバーライド

このガイドでは、既存のNodeblocksスキーマをオーバーライドして拡張し、検証ルールをカスタマイズしたり、新しいフィールドを追加したり、既存のフィールド制約を変更する方法を説明します。組み込みのuserSchemaをカスタム検証ルールと追加必須フィールドで拡張する方法を示します。


🏗️ スキーマオーバーライドアーキテクチャ

スキーマオーバーライドにより以下が可能になります:

  1. 既存スキーマの拡張 - SDKの事前定義スキーマをベースに構築
  2. カスタム検証の追加 - ドメイン固有の検証ルールを実装
  3. フィールド制約の変更 - 最小/最大値、パターン、または型の変更
  4. 必須フィールドの制御 - 必要に応じてフィールドを必須またはオプションに設定

1️⃣ 組み込みスキーマの理解

Nodeblocksは一般的なエンティティ用の事前構築スキーマを提供します。userSchemaには標準的なユーザーフィールドが含まれます:

// 組み込みuserSchemaの構造
{
type: 'object',
properties: {
email: { type: 'string' },
name: { type: 'string' },
status: { type: 'string' }
// ... その他の組み込みプロパティ
},
additionalProperties: false
}

2️⃣ 基本的なスキーマオーバーライド

カスタム検証で既存スキーマをオーバーライドする方法:

src/schemas/customUserSchema.ts
import {
primitives,
schemas,
} from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;
const { userSchema } = schemas;

export const createUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
// 既存スキーマをベースとして使用
...userSchema,
// カスタム必須フィールドを追加
required: ['email', 'name', 'department', 'employeeId'],
properties: {
// 既存プロパティを保持
...userSchema.properties,
// 新しいフィールドを追加
department: {
type: 'string',
enum: ['engineering', 'marketing', 'sales', 'hr']
},
employeeId: {
type: 'string',
pattern: '^EMP-[0-9]{6}$',
description: 'EMP-xxxxxx形式の従業員ID'
},
phoneNumber: {
type: 'string',
pattern: '^[0-9]{3}-[0-9]{3}-[0-9]{4}$',
description: 'xxx-xxx-xxxx形式の電話番号'
},
// 既存フィールドの制約を変更
email: {
type: 'string',
format: 'email',
pattern: '^[a-zA-Z0-9._%+-]+@company\\.com$',
description: '会社ドメインのメールアドレスのみ許可'
},
name: {
type: 'string',
minLength: 2,
maxLength: 50,
pattern: '^[a-zA-Z\\s]+$',
description: '文字とスペースのみを含む名前'
}
}
},
},
},
required: true,
},
});

3️⃣ 高度なスキーマ拡張

より複雑なカスタマイゼーション:

src/schemas/enhancedUserSchema.ts
import { primitives, schemas } from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;
const { userSchema } = schemas;

// カスタムプロフィールスキーマ
const profileSchema = {
type: 'object',
properties: {
bio: { type: 'string', maxLength: 500 },
avatar: { type: 'string', format: 'uri' },
socialLinks: {
type: 'object',
properties: {
linkedin: { type: 'string', format: 'uri' },
twitter: { type: 'string', format: 'uri' },
github: { type: 'string', format: 'uri' }
},
additionalProperties: false
},
preferences: {
type: 'object',
properties: {
theme: { type: 'string', enum: ['light', 'dark'] },
language: { type: 'string', enum: ['en', 'ja', 'es'] },
notifications: {
type: 'object',
properties: {
email: { type: 'boolean' },
push: { type: 'boolean' },
sms: { type: 'boolean' }
},
additionalProperties: false
}
},
additionalProperties: false
}
},
additionalProperties: false
};

// アドレススキーマ
const addressSchema = {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' },
state: { type: 'string' },
zipCode: { type: 'string', pattern: '^[0-9]{5}(-[0-9]{4})?$' },
country: { type: 'string', default: 'JP' }
},
required: ['street', 'city', 'state', 'zipCode'],
additionalProperties: false
};

export const enhancedCreateUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
// 基本ユーザーフィールド(オーバーライド付き)
email: {
type: 'string',
format: 'email',
pattern: '^[a-zA-Z0-9._%+-]+@(company\\.com|partner\\.org)$'
},
name: {
type: 'string',
minLength: 2,
maxLength: 100
},
role: {
type: 'string',
enum: ['user', 'admin', 'moderator', 'guest']
},
status: {
type: 'string',
enum: ['active', 'inactive', 'pending', 'suspended'],
default: 'pending'
},
// 新しい複合フィールド
profile: profileSchema,
address: addressSchema,
// 配列フィールド
skills: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
maxItems: 20
},
certifications: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
issuer: { type: 'string' },
issueDate: { type: 'string', format: 'date' },
expiryDate: { type: 'string', format: 'date' }
},
required: ['name', 'issuer', 'issueDate'],
additionalProperties: false
}
}
},
required: ['email', 'name', 'role'],
additionalProperties: false
},
},
},
required: true,
},
});

4️⃣ 条件付きスキーマ

ユーザーの役割に基づく条件付き検証:

src/schemas/conditionalUserSchema.ts
import { primitives } from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;

export const conditionalUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
email: { type: 'string', format: 'email' },
name: { type: 'string' },
role: {
type: 'string',
enum: ['user', 'admin', 'manager']
},
// 条件付きフィールド
permissions: {
type: 'array',
items: { type: 'string' }
},
department: { type: 'string' },
reportingTo: { type: 'string' }
},
required: ['email', 'name', 'role'],
allOf: [
// 管理者の場合は権限が必要
{
if: {
properties: { role: { const: 'admin' } }
},
then: {
required: ['permissions'],
properties: {
permissions: {
type: 'array',
items: {
type: 'string',
enum: ['read', 'write', 'delete', 'admin']
},
minItems: 1
}
}
}
},
// マネージャーの場合は部署が必要
{
if: {
properties: { role: { const: 'manager' } }
},
then: {
required: ['department']
}
},
// 一般ユーザーの場合は上司が必要
{
if: {
properties: { role: { const: 'user' } }
},
then: {
required: ['reportingTo']
}
}
],
additionalProperties: false
},
},
},
required: true,
},
});

5️⃣ 動的スキーマ生成

設定に基づくスキーマの動的生成:

src/schemas/dynamicUserSchema.ts
import { primitives } from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;

interface SchemaConfig {
strictEmailDomain?: string;
requiredFields?: string[];
allowedRoles?: string[];
maxNameLength?: number;
enableProfile?: boolean;
}

export const createDynamicUserSchema = (config: SchemaConfig) => {
const baseProperties: any = {
email: {
type: 'string',
format: 'email',
...(config.strictEmailDomain && {
pattern: `^[a-zA-Z0-9._%+-]+@${config.strictEmailDomain.replace('.', '\\.')}$`
})
},
name: {
type: 'string',
minLength: 1,
maxLength: config.maxNameLength || 50
},
role: {
type: 'string',
enum: config.allowedRoles || ['user', 'admin']
}
};

// プロフィールフィールドを条件付きで追加
if (config.enableProfile) {
baseProperties.profile = {
type: 'object',
properties: {
bio: { type: 'string', maxLength: 500 },
avatar: { type: 'string', format: 'uri' }
}
};
}

return withSchema({
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: baseProperties,
required: config.requiredFields || ['email', 'name'],
additionalProperties: false
},
},
},
required: true,
},
});
};

// 使用例
export const strictUserSchema = createDynamicUserSchema({
strictEmailDomain: 'company.com',
requiredFields: ['email', 'name', 'role'],
allowedRoles: ['user', 'manager', 'admin'],
maxNameLength: 100,
enableProfile: true
});

export const simpleUserSchema = createDynamicUserSchema({
requiredFields: ['email', 'name'],
allowedRoles: ['user'],
maxNameLength: 30,
enableProfile: false
});

6️⃣ カスタムバリデーター付きスキーマ

src/schemas/validatedUserSchema.ts
import { primitives } from '@nodeblocks/backend-sdk';

const { withSchema } = primitives;

// カスタムバリデーション関数
const validateUniqueEmail = async (email: string, context: any) => {
const existingUser = await context.db.users.findOne({ email });
if (existingUser) {
throw new Error('このメールアドレスは既に使用されています');
}
};

const validateStrongPassword = (password: string) => {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasNonalphas = /\W/.test(password);

if (password.length < minLength) {
throw new Error('パスワードは8文字以上である必要があります');
}

if (!hasUpperCase || !hasLowerCase || !hasNumbers || !hasNonalphas) {
throw new Error('パスワードには大文字、小文字、数字、特殊文字を含める必要があります');
}
};

export const validatedUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
description: '一意でなければならないメールアドレス'
},
password: {
type: 'string',
minLength: 8,
description: '強力なパスワード要件を満たす必要があります'
},
name: {
type: 'string',
minLength: 2,
maxLength: 50
},
dateOfBirth: {
type: 'string',
format: 'date',
description: 'YYYY-MM-DD形式'
},
// カスタム検証付きフィールド
username: {
type: 'string',
pattern: '^[a-zA-Z0-9_]{3,20}$',
description: '3-20文字の英数字とアンダースコア'
}
},
required: ['email', 'password', 'name', 'username'],
additionalProperties: false
},
},
},
required: true,
},
});

// バリデーターミドルウェア
export const customUserValidator = async (payload: any) => {
const { requestBody } = payload.params;

// メールの一意性をチェック
if (requestBody.email) {
await validateUniqueEmail(requestBody.email, payload.context);
}

// パスワード強度をチェック
if (requestBody.password) {
validateStrongPassword(requestBody.password);
}

// 年齢制限をチェック
if (requestBody.dateOfBirth) {
const birthDate = new Date(requestBody.dateOfBirth);
const today = new Date();
const age = today.getFullYear() - birthDate.getFullYear();

if (age < 18) {
throw new Error('18歳以上である必要があります');
}
}
};

7️⃣ 実用的な使用例

カスタムスキーマをサービスで使用:

src/services/customUserService.ts
import { primitives } from '@nodeblocks/backend-sdk';
import { partial } from 'lodash';
import {
enhancedCreateUserSchema,
conditionalUserSchema,
validatedUserSchema,
customUserValidator
} from '../schemas/customUserSchema';
import { createUserRoute, updateUserRoute } from '../routes/user';

const { compose, defService, withRoute } = primitives;

// カスタムスキーマ付きの拡張ルート
const enhancedCreateUserRoute = withRoute({
method: 'POST',
path: '/users/enhanced',
validators: [customUserValidator],
handler: createUserRoute.handler
});

const conditionalCreateUserRoute = withRoute({
method: 'POST',
path: '/users/conditional',
handler: createUserRoute.handler
});

// カスタムフィーチャー
const enhancedCreateUserFeature = compose(
enhancedCreateUserSchema,
enhancedCreateUserRoute
);

const conditionalCreateUserFeature = compose(
conditionalUserSchema,
conditionalCreateUserRoute
);

const validatedCreateUserFeature = compose(
validatedUserSchema,
enhancedCreateUserRoute
);

export const customUserService = (dataStores: any, configuration: any) =>
defService(
partial(
compose(
enhancedCreateUserFeature,
conditionalCreateUserFeature,
validatedCreateUserFeature
),
{ dataStores, configuration }
)
);

8️⃣ テストとデバッグ

カスタムスキーマのテスト:

tests/customSchema.test.ts
import { validateSchema } from '../src/schemas/customUserSchema';

describe('カスタムユーザースキーマ', () => {
test('有効な拡張ユーザーデータを受け入れる', () => {
const validData = {
email: 'user@company.com',
name: 'John Doe',
role: 'user',
profile: {
bio: 'Software developer with 5 years experience',
preferences: {
theme: 'dark',
language: 'en'
}
},
address: {
street: '123 Main St',
city: 'Tokyo',
state: 'Tokyo',
zipCode: '100-0001',
country: 'JP'
},
skills: ['JavaScript', 'TypeScript', 'React']
};

expect(() => validateSchema(validData)).not.toThrow();
});

test('無効なメールドメインを拒否する', () => {
const invalidData = {
email: 'user@wrong-domain.com',
name: 'John Doe',
role: 'user'
};

expect(() => validateSchema(invalidData)).toThrow('会社ドメインのメールアドレスのみ許可');
});

test('必須フィールドの欠落を検出する', () => {
const incompleteData = {
email: 'user@company.com'
// name is missing
};

expect(() => validateSchema(incompleteData)).toThrow('name is required');
});
});

🎯 ベストプラクティス

1. 段階的な拡張

// 小さな変更から開始
const basicExtension = {
...userSchema,
required: [...userSchema.required, 'department']
};

// 徐々に複雑さを追加
const advancedExtension = {
...basicExtension,
properties: {
...basicExtension.properties,
customField: { type: 'string' }
}
};

2. 再利用可能なコンポーネント

// 再利用可能なスキーマ片
const contactInfoSchema = {
email: { type: 'string', format: 'email' },
phone: { type: 'string', pattern: '^[0-9-]+$' }
};

const auditFieldsSchema = {
createdAt: { type: 'string', format: 'date-time' },
updatedAt: { type: 'string', format: 'date-time' }
};

// 複数のスキーマで使用
const userSchema = {
properties: {
...contactInfoSchema,
...auditFieldsSchema,
name: { type: 'string' }
}
};

3. 明確なドキュメント

export const customUserSchema = withSchema({
requestBody: {
content: {
'application/json': {
schema: {
description: 'カスタムユーザー作成スキーマ - 会社固有のフィールド付き',
type: 'object',
properties: {
email: {
type: 'string',
format: 'email',
description: '会社ドメイン(@company.com)のメールアドレスのみ許可'
},
employeeId: {
type: 'string',
pattern: '^EMP-[0-9]{6}$',
description: '6桁の数字を持つEMP-プレフィックス(例: EMP-123456)'
}
}
}
}
}
}
});

⚠️ 注意点

1. パフォーマンスへの影響

// 複雑な検証は慎重に使用
const heavyValidationSchema = {
properties: {
data: {
type: 'array',
items: { /* 複雑なネストスキーマ */ },
maxItems: 1000 // 大量データの制限
}
}
};

2. 下位互換性

// 既存フィールドを削除する代わりに非推奨にする
const backwardCompatibleSchema = {
properties: {
oldField: {
type: 'string',
deprecated: true,
description: '非推奨: 代わりにnewFieldを使用してください'
},
newField: { type: 'string' }
}
};

3. エラーメッセージの国際化

const i18nSchema = {
properties: {
email: {
type: 'string',
format: 'email',
errorMessage: {
type: 'メールアドレスは文字列である必要があります',
format: '有効なメールアドレス形式を入力してください'
}
}
}
};

➡️ 次のステップ