import type * as __ManduDdlPersistenceTypesTypes0 from "./ddl/persistence-types"; /** * Resource Schema Definition * Resource-Centric Architecture의 핵심 스키마 정의 */ import type { z } from "zod"; // ============================================ // Field Types // ============================================ export const FieldTypes = [ "string", "number", "boolean", "date", "uuid", "email", "url", "json", "array", "object", ] as const; export type FieldType = (typeof FieldTypes)[number]; // ============================================ // Field Definition // ============================================ export interface ResourceField { /** 필드 타입 */ type: FieldType; /** 필수 여부 */ required?: boolean; /** 기본값 */ default?: unknown; /** 설명 */ description?: string; /** 배열 타입인 경우 요소 타입 */ items?: FieldType; /** 커스텀 Zod 스키마 (고급 사용) */ schema?: z.ZodTypeAny; } // ============================================ // Resource Options // ============================================ export interface ResourceOptions { /** 리소스 설명 */ description?: string; /** API 태그 */ tags?: string[]; /** 자동 복수형 사용 여부 (기본: true) */ autoPlural?: boolean; /** 커스텀 복수형 이름 */ pluralName?: string; /** 활성화할 엔드포인트 */ endpoints?: { list?: boolean; get?: boolean; create?: boolean; update?: boolean; delete?: boolean; }; /** 인증 필요 여부 */ auth?: boolean; /** 페이지네이션 설정 */ pagination?: { defaultLimit?: number; maxLimit?: number; }; /** * Opt-in DB persistence config (Phase 4c). * * When set, the resource generator emits an additional `.repo.ts` * artifact with typed CRUD backed by `@mandujs/core/db`, and the schema * pipeline (`mandu db plan` / `computeSchemaGeneration`) includes this * resource in migrations. * * The concrete shape lives in * `@mandujs/core/resource/ddl/persistence-types` — imported lazily to * keep `ResourceOptions` usable on runtimes without the DDL subsystem. * Non-persistent resources simply omit this field (existing behavior). * * @example * ```ts * defineResource({ * name: "post", * fields: { ... }, * options: { * persistence: { * provider: "sqlite", * primaryKey: "id", * indexes: [{ name: "posts_user_idx", fields: ["userId"], unique: false }], * fieldOverrides: { id: { primary: true }, userId: { indexed: true } }, * }, * }, * }); * ``` */ persistence?: __ManduDdlPersistenceTypesTypes0.ExtendedResourcePersistence; } // ============================================ // Resource Definition // ============================================ export interface ResourceDefinition { /** 리소스 이름 (단수형) */ name: string; /** 필드 정의 */ fields: Record; /** 옵션 */ options?: ResourceOptions; } // ============================================ // defineResource API // ============================================ /** * Define a resource with fields and options * * @example * ```typescript * const UserResource = defineResource({ * name: "user", * fields: { * id: { type: "uuid", required: true }, * email: { type: "email", required: true }, * name: { type: "string", required: true }, * createdAt: { type: "date", required: true }, * }, * options: { * description: "User management API", * tags: ["users"], * endpoints: { * list: true, * get: true, * create: true, * update: true, * delete: true, * }, * }, * }); * ``` */ export function defineResource(definition: ResourceDefinition): ResourceDefinition { // Validation validateResourceDefinition(definition); // Default options const options: ResourceOptions = { autoPlural: true, endpoints: { list: true, get: true, create: true, update: true, delete: true, }, pagination: { defaultLimit: 10, maxLimit: 100, }, ...definition.options, }; return { ...definition, options, }; } // ============================================ // Validation // ============================================ /** * Validate resource definition */ export function validateResourceDefinition(definition: ResourceDefinition): void { // Name validation if (!definition.name) { throw new Error("Resource name is required"); } if (!/^[a-z][a-z0-9_]*$/i.test(definition.name)) { throw new Error( `Invalid resource name: "${definition.name}". Must start with a letter and contain only letters, numbers, and underscores.` ); } // Fields validation if (!definition.fields || Object.keys(definition.fields).length === 0) { throw new Error(`Resource "${definition.name}" must have at least one field`); } for (const [fieldName, field] of Object.entries(definition.fields)) { validateField(definition.name, fieldName, field); } } /** * Validate individual field */ function validateField(resourceName: string, fieldName: string, field: ResourceField): void { // Field name validation if (!/^[a-z][a-z0-9_]*$/i.test(fieldName)) { throw new Error( `Invalid field name: "${fieldName}" in resource "${resourceName}". Must start with a letter and contain only letters, numbers, and underscores.` ); } // Type validation if (!FieldTypes.includes(field.type)) { throw new Error( `Invalid field type: "${field.type}" for field "${fieldName}" in resource "${resourceName}". Must be one of: ${FieldTypes.join(", ")}` ); } // Array type requires items if (field.type === "array" && !field.items && !field.schema) { throw new Error( `Field "${fieldName}" in resource "${resourceName}" is array type but missing "items" property` ); } } // ============================================ // Helper Functions // ============================================ /** * Get plural name for resource */ export function getPluralName(definition: ResourceDefinition): string { if (definition.options?.pluralName) { return definition.options.pluralName; } if (definition.options?.autoPlural === false) { return definition.name; } return pluralize(definition.name); } function pluralize(singular: string): string { if (/[^aeiou]y$/i.test(singular)) { return singular.slice(0, -1) + "ies"; } if (/(?:s|x|z|ch|sh)$/i.test(singular)) { return singular + "es"; } return singular + "s"; } /** * Get enabled endpoints */ export function getEnabledEndpoints(definition: ResourceDefinition): string[] { const endpoints = definition.options?.endpoints ?? { list: true, get: true, create: true, update: true, delete: true, }; return Object.entries(endpoints) .filter(([_, enabled]) => enabled) .map(([name]) => name); } /** * Check if field is required */ export function isFieldRequired(field: ResourceField): boolean { return field.required ?? false; } /** * Get field default value */ export function getFieldDefault(field: ResourceField): unknown | undefined { return field.default; }