up
All checks were successful
Deploy to Production / deploy (push) Successful in 8s

This commit is contained in:
lubukhu
2026-01-24 13:35:11 +07:00
parent 6c3e93636e
commit 65fd0158a3
145 changed files with 10262 additions and 0 deletions

View File

@@ -0,0 +1,245 @@
/**
* Data Validator
* Verify data structure cho từng game code
*
* Usage:
* ```typescript
* import { validateGameData, DataValidator } from 'game-iframe-sdk/client';
*
* const result = validateGameData('G001', receivedData);
* if (!result.valid) {
* console.error('Invalid data:', result.errors);
* }
* ```
*/
import { GAME_CODES } from '../kit/GameDataHandler';
// =============================================================================
// SCHEMAS FOR EACH GAME CODE
// =============================================================================
const QUIZ_BASE_SCHEMA = {
id: { type: 'string', required: true, description: 'Unique question ID' },
options: { type: 'array', required: true, arrayItemType: 'string', description: 'Answer options' },
answer: { type: 'number', required: true, description: 'Correct answer index (0-based)' },
};
const SCHEMAS = {
// Quiz variants
G001: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
},
G002: {
...QUIZ_BASE_SCHEMA,
question_audio: { type: 'string', required: true, description: 'Audio URL for question' },
},
G003: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are audio URLs
},
G004: {
...QUIZ_BASE_SCHEMA,
question_image: { type: 'string', required: true, description: 'Image URL for question' },
question: { type: 'string', required: false, description: 'Optional text hint' },
},
// G005: Quiz Text-Image (options are image URLs, client picks index)
G005: {
...QUIZ_BASE_SCHEMA,
question: { type: 'string', required: true, description: 'Text question' },
// options are image URLs, answer is index pointing to correct image
},
// Sequence Word variants
G110: {
id: { type: 'string', required: true },
word: { type: 'string', required: true, description: 'The word to arrange' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Letters/parts to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct order' },
},
G111: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true, description: 'Audio hint URL' },
},
G112: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G113: {
id: { type: 'string', required: true },
word: { type: 'string', required: true },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
// Sequence Sentence variants
G120: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false, description: 'Full sentence (hint)' },
parts: { type: 'array', required: true, arrayItemType: 'string', description: 'Words to arrange' },
answer: { type: 'array', required: true, arrayItemType: 'string', description: 'Correct word order' },
},
G121: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G122: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
},
G123: {
id: { type: 'string', required: true },
sentence: { type: 'string', required: false },
parts: { type: 'array', required: true, arrayItemType: 'string' },
answer: { type: 'array', required: true, arrayItemType: 'string' },
audio_url: { type: 'string', required: true },
}
};
// =============================================================================
// VALIDATOR
// =============================================================================
/**
* Validate a single item against schema
*/
function validateItem(item, schema, itemIndex) {
const errors = [];
if (!item || typeof item !== 'object') {
errors.push(`Item [${itemIndex}]: Must be an object`);
return errors;
}
for (const [field, fieldSchema] of Object.entries(schema)) {
const value = item[field];
// Check required
if (fieldSchema.required && (value === undefined || value === null)) {
errors.push(`Item [${itemIndex}].${field}: Required field is missing`);
continue;
}
// Skip validation if optional and not present
if (!fieldSchema.required && (value === undefined || value === null)) {
continue;
}
// Check type
const actualType = Array.isArray(value) ? 'array' : typeof value;
if (fieldSchema.type !== 'any' && actualType !== fieldSchema.type) {
errors.push(`Item [${itemIndex}].${field}: Expected ${fieldSchema.type}, got ${actualType}`);
continue;
}
// Check array items
if (fieldSchema.type === 'array' && fieldSchema.arrayItemType && fieldSchema.arrayItemType !== 'any') {
for (let i = 0; i < value.length; i++) {
const itemType = typeof value[i];
if (itemType !== fieldSchema.arrayItemType) {
errors.push(`Item [${itemIndex}].${field}[${i}]: Expected ${fieldSchema.arrayItemType}, got ${itemType}`);
}
}
}
}
return errors;
}
/**
* Validate game data payload
*/
export function validateGameData(gameCode, payload) {
const errors = [];
const warnings = [];
// Check game code
if (!GAME_CODES[gameCode]) {
errors.push(`Unknown game code: ${gameCode}`);
return { valid: false, errors, warnings };
}
// Check payload structure
if (!payload || typeof payload !== 'object') {
errors.push('Payload must be an object');
return { valid: false, errors, warnings };
}
// Check data array
const items = payload.data || payload.items || payload.questions;
if (!items) {
errors.push('Missing data array (expected "data", "items", or "questions")');
return { valid: false, errors, warnings };
}
if (!Array.isArray(items)) {
errors.push('"data" must be an array');
return { valid: false, errors, warnings };
}
if (items.length === 0) {
warnings.push('Data array is empty');
}
// Validate each item
const schema = SCHEMAS[gameCode];
for (let i = 0; i < items.length; i++) {
const itemErrors = validateItem(items[i], schema, i);
errors.push(...itemErrors);
}
// Check for duplicate IDs
const ids = items.map((item) => item.id).filter(Boolean);
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
if (duplicates.length > 0) {
warnings.push(`Duplicate IDs found: ${[...new Set(duplicates)].join(', ')}`);
}
return {
valid: errors.length === 0,
errors,
warnings,
};
}
/**
* Get schema for a game code
*/
export function getSchema(gameCode) {
return SCHEMAS[gameCode] ?? null;
}
/**
* Get schema documentation for a game code
*/
export function getSchemaDoc(gameCode) {
const schema = SCHEMAS[gameCode];
if (!schema)
return `Unknown game code: ${gameCode}`;
const gameInfo = GAME_CODES[gameCode];
const lines = [
`## ${gameCode}: ${gameInfo.name}`,
`Category: ${gameInfo.category}`,
'',
'### Fields:',
];
for (const [field, fieldSchema] of Object.entries(schema)) {
const required = fieldSchema.required ? '(required)' : '(optional)';
let type = fieldSchema.type;
if (fieldSchema.arrayItemType) {
type = `${fieldSchema.type}<${fieldSchema.arrayItemType}>`;
}
lines.push(`- **${field}**: ${type} ${required}`);
if (fieldSchema.description) {
lines.push(` - ${fieldSchema.description}`);
}
}
return lines.join('\n');
}
// =============================================================================
// DATA VALIDATOR CLASS
// =============================================================================
export class DataValidator {
constructor(gameCode) {
this.gameCode = gameCode;
}
validate(payload) {
return validateGameData(this.gameCode, payload);
}
getSchema() {
return getSchema(this.gameCode);
}
getSchemaDoc() {
return getSchemaDoc(this.gameCode);
}
}
//# sourceMappingURL=DataValidator.js.map