@spfn/cms 0.2.0-beta.3 → 0.2.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -511,7 +511,7 @@ var cmsAppRouter = defineRouter({
511
511
  });
512
512
 
513
513
  // src/server/services/sync.service.ts
514
- import { isEqual } from "lodash";
514
+ import { isEqual } from "lodash-es";
515
515
 
516
516
  // src/lib/helpers.ts
517
517
  function flattenLabels(labels, prefix = "") {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.ts","../src/server/routes/index.ts","../src/server/repositories/cms-labels.repository.ts","../src/server/entities/cms-labels.ts","../src/server/entities/cms-schema.ts","../src/server/entities/cms-label-values.ts","../src/server/entities/cms-published-cache.ts","../src/server/repositories/cms-label-values.repository.ts","../src/server/repositories/cms-published-cache.repository.ts","../src/server/services/sync.service.ts","../src/lib/helpers.ts"],"sourcesContent":["import '@spfn/cms/config';\n\nexport { cmsAppRouter } from './server/routes';\nexport { syncLabels } from './server/services';","/**\n * CMS App Router\n *\n * 모든 CMS 라우트를 통합하는 메인 라우터\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { cmsPublishedCacheRepository } from '../repositories';\n\nexport const getLabelCache = route.get('/_cms/labels/cache')\n .skip(['auth'])\n .input({\n query: Type.Object({\n sections: Type.Array(Type.String()),\n locale: Type.Optional(Type.String())\n })\n })\n .handler(async (c) =>\n {\n const { query } = await c.data();\n const { sections, locale = 'en' } = query;\n\n // 단일 쿼리로 모든 섹션 조회 (N+1 방지)\n const results = await cmsPublishedCacheRepository.findBySections(sections, locale);\n\n // Record<section, content> 형태로 변환\n return results.reduce((acc, item) => {\n acc[item.section] = item.content;\n return acc;\n }, {} as Record<string, any>);\n });\n\nexport const cmsAppRouter = defineRouter({\n getLabelCache\n});\n\nexport type AppRouter = typeof cmsAppRouter;","/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { asc, count as drizzleCount, eq, inArray } from 'drizzle-orm';\nimport { type CmsLabel, cmsLabels, type NewCmsLabel } from '../entities';\n\n/**\n * CMS Labels Repository 클래스\n *\n * BaseRepository를 상속받아 다음 기능을 제공:\n * - 자동 트랜잭션 컨텍스트 감지 및 사용\n * - Read/Write 연결 분리 (replica 활용)\n * - 타입 안전성\n */\nexport class CmsLabelsRepository extends BaseRepository\n{\n /**\n * 라벨 목록 조회\n * Read replica 사용\n */\n async findMany(options?: {\n section?: string;\n }): Promise<CmsLabel[]>\n {\n const { section } = options || {};\n\n let query = this.readDb\n .select()\n .from(cmsLabels)\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n\n if (section)\n {\n query = query.where(eq(cmsLabels.section, section)) as typeof query;\n }\n\n return query;\n }\n\n /**\n * 전체 라벨 수 조회\n * Read replica 사용\n */\n async count(section?: string): Promise<number>\n {\n const query = this.readDb\n .select({ count: drizzleCount() })\n .from(cmsLabels);\n\n const result = section\n ? await query.where(eq(cmsLabels.section, section))\n : await query;\n\n return result[0]?.count ?? 0;\n }\n\n /**\n * ID로 라벨 조회\n * Read replica 사용\n */\n async findById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * Key로 라벨 조회\n * Read replica 사용\n */\n async findByKey(key: string): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.key, key))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 섹션으로 모든 라벨 조회\n * Read replica 사용\n */\n async findBySection(section: string): Promise<CmsLabel[]>\n {\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.section, section))\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n }\n\n /**\n * 라벨 생성\n * Write primary 사용\n */\n async create(data: NewCmsLabel): Promise<CmsLabel>\n {\n const result = await this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n\n return result[0];\n }\n\n /**\n * 라벨 수정\n * Write primary 사용\n */\n async updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n {\n const result = await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 라벨 삭제\n * Write primary 사용\n */\n async deleteById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.db\n .delete(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 여러 key로 라벨 조회\n * Read replica 사용\n */\n async findByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(inArray(cmsLabels.key, keys));\n }\n\n /**\n * 여러 라벨 한번에 생성\n * Write primary 사용\n */\n async bulkCreate(data: NewCmsLabel[]): Promise<CmsLabel[]>\n {\n if (data.length === 0)\n {\n return [];\n }\n\n return this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n }\n\n /**\n * 여러 라벨 한번에 수정 (key 기준)\n * Write primary 사용\n *\n * @param updates - Array of { key, data } objects\n */\n async bulkUpdateByKeys(updates: Array<{ key: string; data: Partial<NewCmsLabel> }>): Promise<void>\n {\n if (updates.length === 0)\n {\n return;\n }\n\n // Drizzle doesn't support bulk update directly, so we need to do it one by one\n // But we can do it in a single transaction context (handled by BaseRepository)\n for (const { key, data } of updates)\n {\n await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.key, key));\n }\n }\n\n /**\n * 여러 라벨 한번에 삭제 (key 기준)\n * Write primary 사용\n */\n async bulkDeleteByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.db\n .delete(cmsLabels)\n .where(inArray(cmsLabels.key, keys))\n .returning();\n }\n}\n\n// Default instance export\nexport const cmsLabelsRepository = new CmsLabelsRepository();\n\n","/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, text } from 'drizzle-orm/pg-core';\nimport { id, timestamps, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\nexport const cmsLabels = cmsSchema.table('labels', {\n // Primary Key\n id: id(),\n\n // 라벨 식별자\n key: text('key').notNull().unique(),\n // 예: \"home.hero.title\", \"why-futureplay.hero.subtitle\"\n // 구조: {section}.{component}.{property}\n\n // 섹션 분류 (페이지 단위)\n section: text('section').notNull(),\n // 예: \"home\", \"why-futureplay\", \"team\"\n\n // 값 타입\n type: text('type').notNull(),\n // \"text\" | \"image\" | \"video\" | \"file\" | \"object\"\n\n // 기본값\n defaultValue: typedJsonb<Record<string, any>>('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n // 예: { en: \"Welcome\", ko: \"환영합니다\" } 또는 단일 값\n\n // 설명\n description: text('description'),\n // 라벨에 대한 설명 (optional)\n\n // 현재 발행된 버전 번호\n publishedVersion: integer('published_version'),\n // null = 미발행 상태\n // 1, 2, 3... = 발행된 버전 번호\n\n // 생성자 추적\n createdBy: text('created_by'),\n\n // 타임스탬프\n ...timestamps(),\n}, (table) => [\n // 인덱스: 섹션별 조회 최적화\n index('cms_labels_section_idx').on(table.section),\n\n // 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)\n index('cms_labels_key_idx').on(table.key),\n]);\n\n// 타입 추론\nexport type CmsLabel = typeof cmsLabels.$inferSelect;\nexport type NewCmsLabel = typeof cmsLabels.$inferInsert;","/**\n * CMS Schema Definition\n *\n * Creates isolated 'spfn_cms' PostgreSQL schema for CMS tables.\n * Export this schema so drizzle-kit can generate CREATE SCHEMA statement.\n */\nimport { createSchema } from '@spfn/core/db';\n\nexport const cmsSchema = createSchema('@spfn/cms');","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { integer, text, index, unique } from 'drizzle-orm/pg-core';\nimport { id, utcTimestamp, typedJsonb, foreignKey } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\nimport { cmsLabels } from './cms-labels';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsLabelValues = cmsSchema.table('label_values', {\n // Primary Key\n id: id(),\n\n // Foreign Key: cms_labels\n labelId: foreignKey('label', () => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('en'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: typedJsonb<Record<string, any>>('value').notNull(),\n // LabelValue 타입:\n // - TextValue: { type: \"text\", content: string }\n // - ImageValue: { type: \"image\", url: string, alt?: string, width?: number, height?: number }\n // - VideoValue: { type: \"video\", url: string, thumbnail?: string, duration?: number }\n // - FileValue: { type: \"file\", url: string, filename: string, size?: number }\n // - ObjectValue: { type: \"object\", fields: Record<string, LabelValue> }\n\n // 생성 시각\n createdAt: utcTimestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n // UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일\n unique('cms_label_values_locale_breakpoint_unique')\n .on(table.labelId, table.version, table.locale, table.breakpoint),\n\n // 인덱스: labelId + version 복합 조회 최적화\n index('cms_label_values_label_version_idx')\n .on(table.labelId, table.version),\n\n // 인덱스: locale 필터링 최적화\n index('cms_label_values_locale_idx').on(table.locale),\n]);\n\n// 타입 추론\nexport type CmsLabelValue = typeof cmsLabelValues.$inferSelect;\nexport type NewCmsLabelValue = typeof cmsLabelValues.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 텍스트 값 저장\n * await db.insert(cmsLabelValues).values({\n * labelId: 1,\n * version: 1,\n * locale: 'ko',\n * breakpoint: null,\n * value: {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * }\n * });\n *\n * // 반응형 이미지 저장 (모바일용)\n * await db.insert(cmsLabelValues).values({\n * labelId: 2,\n * version: 1,\n * locale: 'ko',\n * breakpoint: 'sm',\n * value: {\n * type: 'image',\n * url: '/uploads/hero-mobile.jpg',\n * alt: 'Hero Image',\n * width: 640,\n * height: 480\n * }\n * });\n *\n * // 특정 버전의 한국어 값 조회\n * const values = await db.select()\n * .from(cmsLabelValues)\n * .where(and(\n * eq(cmsLabelValues.labelId, 1),\n * eq(cmsLabelValues.version, 2),\n * eq(cmsLabelValues.locale, 'ko')\n * ));\n *\n * // Object 타입 값 저장 (재귀 구조)\n * await db.insert(cmsLabelValues).values({\n * labelId: 3,\n * version: 1,\n * locale: 'ko',\n * value: {\n * type: 'object',\n * fields: {\n * title: { type: 'text', content: '특징 1' },\n * icon: { type: 'image', url: '/icons/feature1.svg', alt: 'Icon' },\n * description: { type: 'text', content: '상세 설명...' }\n * }\n * }\n * });\n */","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { text, integer, index, unique } from 'drizzle-orm/pg-core';\nimport { id, publishingFields, typedJsonb } from \"@spfn/core/db\";\nimport { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\nexport const cmsPublishedCache = cmsSchema.table('published_cache', {\n // Primary Key\n id: id(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 캐시된 콘텐츠 (JSONB)\n content: typedJsonb<Record<string, any>>('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"...\" },\n // \"home.hero.image\": { type: \"image\", url: \"...\", alt: \"...\" },\n // ...\n // }\n\n // 발행 정보\n ...publishingFields(),\n\n // 캐시 버전 (클라이언트 캐싱용)\n version: integer('version').notNull().default(1),\n}, (table) => [\n // UNIQUE 제약: section + locale 조합은 유일\n unique('cms_published_cache_unique').on(table.section, table.locale),\n\n // 인덱스: section으로 조회 최적화\n index('cms_published_cache_section_idx').on(table.section),\n]);\n\n// 타입 추론\nexport type CmsPublishedCache = typeof cmsPublishedCache.$inferSelect;\nexport type NewCmsPublishedCache = typeof cmsPublishedCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 캐시 생성/업데이트 (UPSERT)\n * await db.insert(cmsPublishedCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * content: {\n * 'home.hero.title': {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * },\n * 'home.hero.image': {\n * type: 'image',\n * url: '/uploads/hero.jpg',\n * alt: 'Hero',\n * width: 1920,\n * height: 1080\n * }\n * },\n * publishedAt: new Date(),\n * publishedBy: 'admin@futureplay.com'\n * })\n * .onConflictDoUpdate({\n * target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n * set: {\n * content: sql`EXCLUDED.content`,\n * publishedAt: sql`EXCLUDED.published_at`,\n * publishedBy: sql`EXCLUDED.published_by`,\n * version: sql`${cmsPublishedCache.version} + 1`\n * }\n * });\n *\n * // 캐시 조회 (초고속!)\n * const cache = await db.select()\n * .from(cmsPublishedCache)\n * .where(and(\n * eq(cmsPublishedCache.section, 'home'),\n * eq(cmsPublishedCache.locale, 'ko')\n * ))\n * .limit(1);\n *\n * const labels = cache[0].content; // 즉시 사용 가능!\n *\n * // 섹션의 모든 언어 캐시 조회\n * const allLocales = await db.select()\n * .from(cmsPublishedCache)\n * .where(eq(cmsPublishedCache.section, 'home'));\n *\n * // 오래된 캐시 감지\n * const stale = await db.select()\n * .from(cmsPublishedCache)\n * .where(lt(\n * cmsPublishedCache.publishedAt,\n * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)\n * ));\n */","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, SQL, isNull, gte, lte, inArray } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\n/**\n * 버전 히스토리 타입\n */\nexport interface VersionHistory\n{\n version: number;\n publishedAt: string;\n publishedBy: null;\n notes: null;\n values: Array<{\n id: number;\n locale: string;\n breakpoint: string | null;\n value: any;\n createdAt: string;\n }>;\n}\n\n/**\n * CMS Label Values Repository 클래스\n */\nexport class CmsLabelValuesRepository extends BaseRepository\n{\n /**\n * 특정 라벨의 특정 버전 값들 조회\n * Read replica 사용\n */\n async findByLabelIdAndVersion(\n labelId: number,\n version: number,\n options?: {\n locale?: string;\n breakpoint?: string | null;\n }\n ): Promise<CmsLabelValue[]>\n {\n const { locale, breakpoint } = options || {};\n\n const conditions: SQL[] = [\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n ];\n\n if (locale)\n {\n conditions.push(eq(cmsLabelValues.locale, locale));\n }\n\n if (breakpoint !== undefined)\n {\n conditions.push(\n breakpoint === null\n ? isNull(cmsLabelValues.breakpoint)\n : eq(cmsLabelValues.breakpoint, breakpoint)\n );\n }\n\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(and(...conditions));\n }\n\n /**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n * Write primary 사용\n */\n async upsert(data: NewCmsLabelValue & { labelId: number }): Promise<CmsLabelValue>\n {\n // 기존 값이 있는지 확인\n const versionCondition = data.version === null || data.version === undefined\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, data.version as number);\n\n const existingResult = await this.db\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, data.labelId),\n versionCondition,\n eq(cmsLabelValues.locale, data.locale || 'ko'),\n data.breakpoint\n ? eq(cmsLabelValues.breakpoint, data.breakpoint)\n : isNull(cmsLabelValues.breakpoint)\n )\n )\n .limit(1);\n\n const existing = existingResult[0];\n\n if (existing)\n {\n // UPDATE (only for drafts with version: null)\n if (data.version === null || data.version === undefined)\n {\n const updated = await this.db\n .update(cmsLabelValues)\n .set({ value: data.value })\n .where(eq(cmsLabelValues.id, existing.id))\n .returning();\n\n return updated[0];\n }\n else\n {\n // Published versions are immutable - this shouldn't happen\n throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);\n }\n }\n else\n {\n // INSERT (both draft and new published versions)\n const inserted = await this.db\n .insert(cmsLabelValues)\n .values(data)\n .returning();\n\n return inserted[0];\n }\n }\n\n /**\n * Draft 값들 조회 (version = null)\n * Read replica 사용\n */\n async findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n {\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version)\n )\n );\n }\n\n /**\n * 여러 값 일괄 저장\n * Write primary 사용\n */\n async upsertMany(values: (NewCmsLabelValue & { labelId: number })[]): Promise<CmsLabelValue[]>\n {\n const results = [];\n for (const value of values)\n {\n const result = await this.upsert(value);\n results.push(result);\n }\n return results;\n }\n\n /**\n * 특정 버전의 모든 값 삭제\n * Write primary 사용\n */\n async deleteByVersion(labelId: number, version: number): Promise<CmsLabelValue[]>\n {\n return this.db\n .delete(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n )\n )\n .returning();\n }\n\n /**\n * 여러 라벨의 publishedVersion 값들을 한 번에 조회 (N+1 문제 해결)\n * Read replica 사용\n *\n * @param labelVersions - { labelId, version } 배열\n * @returns labelId를 키로 하는 Map<labelId, CmsLabelValue[]>\n *\n * @example\n * ```typescript\n * const result = await findByLabelVersions([\n * { labelId: 1, version: 5 },\n * { labelId: 2, version: 3 }\n * ]);\n * // result.get(1) -> label 1의 version 5 값들\n * // result.get(2) -> label 2의 version 3 값들\n * ```\n */\n async findByLabelVersions(\n labelVersions: Array<{ labelId: number; version: number }>\n ): Promise<Map<number, CmsLabelValue[]>>\n {\n if (labelVersions.length === 0)\n {\n return new Map();\n }\n\n // 모든 label의 publishedVersion 값들을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n inArray(\n cmsLabelValues.labelId,\n labelVersions.map(lv => lv.labelId)\n )\n )\n );\n\n // labelId와 version으로 필터링하여 Map 생성\n const versionMap = new Map(labelVersions.map(lv => [lv.labelId, lv.version]));\n const resultMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n const expectedVersion = versionMap.get(value.labelId);\n\n // 해당 labelId의 version이 일치하는 경우만 포함\n if (expectedVersion !== undefined && value.version === expectedVersion)\n {\n if (!resultMap.has(value.labelId))\n {\n resultMap.set(value.labelId, []);\n }\n resultMap.get(value.labelId)!.push(value);\n }\n }\n\n return resultMap;\n }\n\n /**\n * 라벨의 버전 히스토리 조회 (1 ~ maxVersion)\n * 한 번의 쿼리로 모든 버전을 조회하고 version별로 그룹화\n * Read replica 사용\n */\n async findVersionHistoryByLabelId(\n labelId: number,\n maxVersion: number\n ): Promise<VersionHistory[]>\n {\n // 모든 버전의 값을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n gte(cmsLabelValues.version, 1),\n lte(cmsLabelValues.version, maxVersion)\n )\n )\n .orderBy(cmsLabelValues.version, cmsLabelValues.locale);\n\n // version별로 그룹화\n const versionMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n if (value.version === null) continue; // null 버전은 제외\n\n if (!versionMap.has(value.version))\n {\n versionMap.set(value.version, []);\n }\n versionMap.get(value.version)!.push(value);\n }\n\n // VersionHistory 형식으로 변환\n const versions: VersionHistory[] = [];\n\n for (let version = 1; version <= maxVersion; version++)\n {\n const values = versionMap.get(version);\n\n if (values && values.length > 0)\n {\n versions.push({\n version,\n publishedAt: values[0].createdAt.toISOString(),\n publishedBy: null, // label_values에는 publishedBy 정보가 없음\n notes: null, // label_values에는 notes 정보가 없음\n values: values.map(v => ({\n id: v.id,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString()\n }))\n });\n }\n }\n\n // 버전 내림차순 정렬 (최신 버전이 먼저)\n versions.sort((a, b) => b.version - a.version);\n\n return versions;\n }\n}\n\n// Default instance export\nexport const cmsLabelValuesRepository = new CmsLabelValuesRepository();","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, sql, inArray } from 'drizzle-orm';\nimport { cmsPublishedCache, type CmsPublishedCache, type NewCmsPublishedCache } from '../entities';\n\n/**\n * CMS Published Cache Repository 클래스\n */\nexport class CmsPublishedCacheRepository extends BaseRepository\n{\n /**\n * 섹션 + 언어로 발행된 캐시 조회\n * Read replica 사용\n */\n async findBySection(section: string, locale: string = 'en'): Promise<CmsPublishedCache | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n )\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 캐시 생성 또는 업데이트 (UPSERT)\n * Write primary 사용\n */\n async upsert(data: NewCmsPublishedCache): Promise<CmsPublishedCache>\n {\n const result = await this.db\n .insert(cmsPublishedCache)\n .values(data)\n .onConflictDoUpdate({\n target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n set: {\n content: data.content,\n publishedAt: data.publishedAt,\n publishedBy: data.publishedBy,\n version: sql`${cmsPublishedCache.version} + 1`, // 버전 증가로 클라이언트 캐시 무효화\n }\n })\n .returning();\n\n return result[0];\n }\n\n /**\n * 여러 섹션의 캐시를 한 번에 조회 (N+1 방지)\n * Read replica 사용\n */\n async findBySections(sections: string[], locale: string = 'en'): Promise<CmsPublishedCache[]>\n {\n if (sections.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n inArray(cmsPublishedCache.section, sections),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n\n /**\n * 섹션별 모든 언어 캐시 조회\n * Read replica 사용\n */\n async findAllLanguages(section: string): Promise<CmsPublishedCache[]>\n {\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n\n /**\n * 캐시 삭제\n * Write primary 사용\n */\n async deleteBySection(section: string, locale?: string): Promise<void>\n {\n if (locale)\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n else\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n }\n}\n\n// Default instance export\nexport const cmsPublishedCacheRepository = new CmsPublishedCacheRepository();","/**\n * CMS Label Synchronization Service\n *\n * Synchronizes labels defined in code with database\n */\n\nimport { isEqual } from 'lodash';\nimport { type SyncOptions, type SyncResult } from '../../lib/types';\nimport { type FlatLabel, flattenLabels } from '../../lib/helpers';\nimport { cmsLabelsRepository } from '../repositories';\nimport { type NewCmsLabel } from '../entities';\n\n/**\n * Compare current DB labels with new labels\n *\n * @param dbLabels - Labels currently in database\n * @param codeLabels - Labels from code (flattened)\n * @returns Comparison result with added/removed/updated labels\n */\nfunction compareLabels(dbLabels: FlatLabel, codeLabels: FlatLabel): SyncResult\n{\n const added: string[] = [];\n const removed: string[] = [];\n const updated: string[] = [];\n const unchanged: string[] = [];\n\n const dbKeys = Object.keys(dbLabels);\n const codeKeys = Object.keys(codeLabels);\n\n // Check for added and updated labels\n for (const key of codeKeys)\n {\n if (!(key in dbLabels))\n {\n // New label\n added.push(key);\n }\n else\n {\n // Check if values changed (deep equality check for nested objects)\n const dbValue = dbLabels[key];\n const codeValue = codeLabels[key];\n\n if (!isEqual(dbValue, codeValue))\n {\n updated.push(key);\n }\n else\n {\n unchanged.push(key);\n }\n }\n }\n\n // Check for removed labels\n for (const key of dbKeys)\n {\n if (!(key in codeLabels))\n {\n removed.push(key);\n }\n }\n\n return {\n added,\n removed,\n updated,\n unchanged,\n };\n}\n\n/**\n * Sync labels with database\n *\n * @param labels - Single label definition or array of label definitions\n * @param options - Sync options\n * @returns Sync result\n *\n * @example\n * ```typescript\n * // Single definition\n * await syncLabels(labelsDefinition);\n *\n * // Multiple definitions\n * await syncLabels([homeLabels, aboutLabels, commonLabels]);\n * ```\n */\nexport async function syncLabels<T extends Record<string, any>>(\n labels: T | T[],\n options?: SyncOptions\n): Promise<SyncResult>\n{\n const { removeOrphaned = false, dryRun = false } = options || {};\n\n // 1. Merge multiple label definitions into one (if array provided)\n const mergedLabels = Array.isArray(labels)\n ? Object.assign({}, ...labels)\n : labels;\n\n // 2. Flatten code labels\n const codeLabels = flattenLabels(mergedLabels);\n\n // 3. Fetch current labels from DB\n const dbLabels = await cmsLabelsRepository.findMany();\n const dbLabelMap: FlatLabel = {};\n\n for (const label of dbLabels)\n {\n if (label.defaultValue)\n {\n dbLabelMap[label.key] = label.defaultValue as Record<string, string>;\n }\n }\n\n // 4. Compare changes\n const result = compareLabels(dbLabelMap, codeLabels);\n\n // 5. Return result if dry run\n if (dryRun)\n {\n return result;\n }\n\n // 6. Create new labels\n if (result.added.length > 0)\n {\n const toCreate: NewCmsLabel[] = result.added.map(key => ({\n key,\n section: extractSection(key),\n type: 'text',\n defaultValue: codeLabels[key],\n }));\n\n await cmsLabelsRepository.bulkCreate(toCreate);\n }\n\n // 7. Update changed labels\n if (result.updated.length > 0)\n {\n const updates = result.updated.map(key => ({\n key,\n data: {\n defaultValue: codeLabels[key],\n },\n }));\n\n await cmsLabelsRepository.bulkUpdateByKeys(updates);\n }\n\n // 8. Remove deleted labels (only if option is true)\n if (removeOrphaned && result.removed.length > 0)\n {\n await cmsLabelsRepository.bulkDeleteByKeys(result.removed);\n }\n\n return result;\n}\n\n/**\n * Extract section from key\n * Example: \"home.hero.title\" -> \"home\"\n */\nfunction extractSection(key: string): string\n{\n const parts = key.split('.');\n return parts[0] || key;\n}","/**\n * CMS Helper Functions\n */\n\nexport type FlatLabel = Record<string, Record<string, string>>;\n\n/**\n * Flatten nested label structure into dot notation\n *\n * @param labels - Nested label object\n * @param prefix - Key prefix for recursion\n * @returns Flattened label structure\n *\n * @example\n * ```typescript\n * const nested = {\n * home: {\n * hero: {\n * title: { en: \"Welcome\", ko: \"환영합니다\" }\n * }\n * }\n * };\n *\n * const flat = flattenLabels(nested);\n * // { \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" } }\n * ```\n */\nexport function flattenLabels<T extends Record<string, any>>(labels: T, prefix = ''): FlatLabel\n{\n const result: FlatLabel = {};\n\n if (!labels || typeof labels !== 'object')\n {\n return result;\n }\n\n const obj = labels as Record<string, unknown>;\n\n for (const [key, value] of Object.entries(obj))\n {\n const newKey = prefix ? `${prefix}.${key}` : key;\n\n if (!value || typeof value !== 'object')\n {\n continue;\n }\n\n const valueObj = value as Record<string, unknown>;\n\n // Check if this is a leaf node (locale values: { en: \"...\", ko: \"...\" })\n const isLeaf = Object.values(valueObj).every(v => typeof v === 'string');\n\n if (isLeaf)\n {\n result[newKey] = valueObj as Record<string, string>;\n }\n else\n {\n // Recursively flatten nested structure\n Object.assign(result, flattenLabels(value, newKey));\n }\n }\n\n return result;\n}\n\n/**\n * Set a value in nested object using dot notation path\n *\n * @param target - Target object to modify\n * @param path - Dot notation path (e.g., \"home.hero.title\")\n * @param value - Value to set\n *\n * @example\n * ```typescript\n * const obj = {};\n * setNestedValue(obj, \"home.hero.title\", \"Welcome\");\n * // obj = { home: { hero: { title: \"Welcome\" } } }\n * ```\n */\nexport function setNestedValue(target: any, path: string, value: any): void\n{\n const parts = path.split('.');\n let current = target;\n\n for (let i = 0; i < parts.length - 1; i++)\n {\n const part = parts[i];\n if (!current[part])\n {\n current[part] = {};\n }\n current = current[part];\n }\n\n // Set the leaf value\n const lastPart = parts[parts.length - 1];\n current[lastPart] = value;\n}\n\n/**\n * Unflatten dot notation keys back to nested structure\n *\n * @param flat - Flattened label object\n * @returns Nested label structure\n *\n * @example\n * ```typescript\n * const flat = {\n * \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" },\n * \"home.hero.subtitle\": { en: \"Subtitle\", ko: \"부제목\" }\n * };\n *\n * const nested = unflattenLabels(flat);\n * // {\n * // home: {\n * // hero: {\n * // title: { en: \"Welcome\", ko: \"환영합니다\" },\n * // subtitle: { en: \"Subtitle\", ko: \"부제목\" }\n * // }\n * // }\n * // }\n * ```\n */\nexport function unflattenLabels(flat: FlatLabel): Record<string, any>\n{\n const result: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(flat))\n {\n setNestedValue(result, key, value);\n }\n\n return result;\n}"],"mappings":"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","names":["integer","text","index","id","typedJsonb","id","integer","text","typedJsonb","index","text","integer","index","unique","id","typedJsonb","id","text","typedJsonb","integer","unique","index","id","BaseRepository","eq","inArray","BaseRepository","eq","inArray","BaseRepository","eq","and","inArray","BaseRepository","and","eq","inArray"]}
1
+ {"version":3,"sources":["../src/server.ts","../src/server/routes/index.ts","../src/server/repositories/cms-labels.repository.ts","../src/server/entities/cms-labels.ts","../src/server/entities/cms-schema.ts","../src/server/entities/cms-label-values.ts","../src/server/entities/cms-published-cache.ts","../src/server/repositories/cms-label-values.repository.ts","../src/server/repositories/cms-published-cache.repository.ts","../src/server/services/sync.service.ts","../src/lib/helpers.ts"],"sourcesContent":["import '@spfn/cms/config';\n\nexport { cmsAppRouter } from './server/routes';\nexport { syncLabels } from './server/services';","/**\n * CMS App Router\n *\n * 모든 CMS 라우트를 통합하는 메인 라우터\n */\n\nimport { Type } from '@sinclair/typebox';\nimport { defineRouter, route } from '@spfn/core/route';\nimport { cmsPublishedCacheRepository } from '../repositories';\n\nexport const getLabelCache = route.get('/_cms/labels/cache')\n .skip(['auth'])\n .input({\n query: Type.Object({\n sections: Type.Array(Type.String()),\n locale: Type.Optional(Type.String())\n })\n })\n .handler(async (c) =>\n {\n const { query } = await c.data();\n const { sections, locale = 'en' } = query;\n\n // 단일 쿼리로 모든 섹션 조회 (N+1 방지)\n const results = await cmsPublishedCacheRepository.findBySections(sections, locale);\n\n // Record<section, content> 형태로 변환\n return results.reduce((acc, item) => {\n acc[item.section] = item.content;\n return acc;\n }, {} as Record<string, any>);\n });\n\nexport const cmsAppRouter = defineRouter({\n getLabelCache\n});\n\nexport type AppRouter = typeof cmsAppRouter;","/**\n * CMS Labels Repository\n *\n * 라벨 메타데이터 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { asc, count as drizzleCount, eq, inArray } from 'drizzle-orm';\nimport { type CmsLabel, cmsLabels, type NewCmsLabel } from '../entities';\n\n/**\n * CMS Labels Repository 클래스\n *\n * BaseRepository를 상속받아 다음 기능을 제공:\n * - 자동 트랜잭션 컨텍스트 감지 및 사용\n * - Read/Write 연결 분리 (replica 활용)\n * - 타입 안전성\n */\nexport class CmsLabelsRepository extends BaseRepository\n{\n /**\n * 라벨 목록 조회\n * Read replica 사용\n */\n async findMany(options?: {\n section?: string;\n }): Promise<CmsLabel[]>\n {\n const { section } = options || {};\n\n let query = this.readDb\n .select()\n .from(cmsLabels)\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n\n if (section)\n {\n query = query.where(eq(cmsLabels.section, section)) as typeof query;\n }\n\n return query;\n }\n\n /**\n * 전체 라벨 수 조회\n * Read replica 사용\n */\n async count(section?: string): Promise<number>\n {\n const query = this.readDb\n .select({ count: drizzleCount() })\n .from(cmsLabels);\n\n const result = section\n ? await query.where(eq(cmsLabels.section, section))\n : await query;\n\n return result[0]?.count ?? 0;\n }\n\n /**\n * ID로 라벨 조회\n * Read replica 사용\n */\n async findById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * Key로 라벨 조회\n * Read replica 사용\n */\n async findByKey(key: string): Promise<CmsLabel | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.key, key))\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 섹션으로 모든 라벨 조회\n * Read replica 사용\n */\n async findBySection(section: string): Promise<CmsLabel[]>\n {\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(eq(cmsLabels.section, section))\n .orderBy(asc(cmsLabels.key)); // key 오름차순 정렬 (JSON 파일의 순서 유지)\n }\n\n /**\n * 라벨 생성\n * Write primary 사용\n */\n async create(data: NewCmsLabel): Promise<CmsLabel>\n {\n const result = await this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n\n return result[0];\n }\n\n /**\n * 라벨 수정\n * Write primary 사용\n */\n async updateById(id: number, data: Partial<NewCmsLabel>): Promise<CmsLabel | null>\n {\n const result = await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 라벨 삭제\n * Write primary 사용\n */\n async deleteById(id: number): Promise<CmsLabel | null>\n {\n const result = await this.db\n .delete(cmsLabels)\n .where(eq(cmsLabels.id, id))\n .returning();\n\n return result[0] ?? null;\n }\n\n /**\n * 여러 key로 라벨 조회\n * Read replica 사용\n */\n async findByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsLabels)\n .where(inArray(cmsLabels.key, keys));\n }\n\n /**\n * 여러 라벨 한번에 생성\n * Write primary 사용\n */\n async bulkCreate(data: NewCmsLabel[]): Promise<CmsLabel[]>\n {\n if (data.length === 0)\n {\n return [];\n }\n\n return this.db\n .insert(cmsLabels)\n .values(data)\n .returning();\n }\n\n /**\n * 여러 라벨 한번에 수정 (key 기준)\n * Write primary 사용\n *\n * @param updates - Array of { key, data } objects\n */\n async bulkUpdateByKeys(updates: Array<{ key: string; data: Partial<NewCmsLabel> }>): Promise<void>\n {\n if (updates.length === 0)\n {\n return;\n }\n\n // Drizzle doesn't support bulk update directly, so we need to do it one by one\n // But we can do it in a single transaction context (handled by BaseRepository)\n for (const { key, data } of updates)\n {\n await this.db\n .update(cmsLabels)\n .set({ ...data, updatedAt: new Date() })\n .where(eq(cmsLabels.key, key));\n }\n }\n\n /**\n * 여러 라벨 한번에 삭제 (key 기준)\n * Write primary 사용\n */\n async bulkDeleteByKeys(keys: string[]): Promise<CmsLabel[]>\n {\n if (keys.length === 0)\n {\n return [];\n }\n\n return this.db\n .delete(cmsLabels)\n .where(inArray(cmsLabels.key, keys))\n .returning();\n }\n}\n\n// Default instance export\nexport const cmsLabelsRepository = new CmsLabelsRepository();\n\n","/**\n * CMS Labels Entity\n *\n * 라벨의 메타데이터와 현재 발행 상태를 관리합니다.\n * - 라벨 식별 (id, key)\n * - 섹션 분류 (section)\n * - 타입 정의 (type)\n * - 발행 상태 (publishedVersion)\n */\n\nimport { index, integer, text } from 'drizzle-orm/pg-core';\nimport { id, timestamps, typedJsonb } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\n\nexport const cmsLabels = cmsSchema.table('labels', {\n // Primary Key\n id: id(),\n\n // 라벨 식별자\n key: text('key').notNull().unique(),\n // 예: \"home.hero.title\", \"why-futureplay.hero.subtitle\"\n // 구조: {section}.{component}.{property}\n\n // 섹션 분류 (페이지 단위)\n section: text('section').notNull(),\n // 예: \"home\", \"why-futureplay\", \"team\"\n\n // 값 타입\n type: text('type').notNull(),\n // \"text\" | \"image\" | \"video\" | \"file\" | \"object\"\n\n // 기본값\n defaultValue: typedJsonb<Record<string, any>>('default_value'),\n // 라벨의 기본값 (sync 시 설정)\n // 예: { en: \"Welcome\", ko: \"환영합니다\" } 또는 단일 값\n\n // 설명\n description: text('description'),\n // 라벨에 대한 설명 (optional)\n\n // 현재 발행된 버전 번호\n publishedVersion: integer('published_version'),\n // null = 미발행 상태\n // 1, 2, 3... = 발행된 버전 번호\n\n // 생성자 추적\n createdBy: text('created_by'),\n\n // 타임스탬프\n ...timestamps(),\n}, (table) => [\n // 인덱스: 섹션별 조회 최적화\n index('cms_labels_section_idx').on(table.section),\n\n // 인덱스: key로 조회 최적화 (unique 제약으로 자동 생성되지만 명시)\n index('cms_labels_key_idx').on(table.key),\n]);\n\n// 타입 추론\nexport type CmsLabel = typeof cmsLabels.$inferSelect;\nexport type NewCmsLabel = typeof cmsLabels.$inferInsert;","/**\n * CMS Schema Definition\n *\n * Creates isolated 'spfn_cms' PostgreSQL schema for CMS tables.\n * Export this schema so drizzle-kit can generate CREATE SCHEMA statement.\n */\nimport { createSchema } from '@spfn/core/db';\n\nexport const cmsSchema = createSchema('@spfn/cms');","/**\n * CMS Label Values Entity\n *\n * 라벨의 실제 값을 저장합니다.\n * - 다국어 지원 (locale)\n * - 반응형 지원 (breakpoint)\n * - 버전 관리 (version)\n * - JSONB로 유연한 값 저장\n */\n\nimport { integer, text, index, unique } from 'drizzle-orm/pg-core';\nimport { id, utcTimestamp, typedJsonb, foreignKey } from '@spfn/core/db';\nimport { cmsSchema } from './cms-schema';\nimport { cmsLabels } from './cms-labels';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\n\nexport const cmsLabelValues = cmsSchema.table('label_values', {\n // Primary Key\n id: id(),\n\n // Foreign Key: cms_labels\n labelId: foreignKey('label', () => cmsLabels.id, { onDelete: 'cascade' }),\n\n // 버전 번호 (null = draft, number = published version)\n version: integer('version'),\n\n // 언어 코드\n locale: text('locale').notNull().default('en'),\n // \"ko\" | \"en\" | \"ja\"\n\n // 반응형 브레이크포인트\n breakpoint: text('breakpoint'),\n // null = 기본값 (모든 화면 크기)\n // \"sm\" | \"md\" | \"lg\" | \"xl\" | \"2xl\"\n\n // 실제 값 (JSONB)\n value: typedJsonb<Record<string, any>>('value').notNull(),\n // LabelValue 타입:\n // - TextValue: { type: \"text\", content: string }\n // - ImageValue: { type: \"image\", url: string, alt?: string, width?: number, height?: number }\n // - VideoValue: { type: \"video\", url: string, thumbnail?: string, duration?: number }\n // - FileValue: { type: \"file\", url: string, filename: string, size?: number }\n // - ObjectValue: { type: \"object\", fields: Record<string, LabelValue> }\n\n // 생성 시각\n createdAt: utcTimestamp('created_at').defaultNow().notNull(),\n}, (table) => [\n // UNIQUE 제약: 같은 버전에서 locale + breakpoint 조합은 유일\n unique('cms_label_values_locale_breakpoint_unique')\n .on(table.labelId, table.version, table.locale, table.breakpoint),\n\n // 인덱스: labelId + version 복합 조회 최적화\n index('cms_label_values_label_version_idx')\n .on(table.labelId, table.version),\n\n // 인덱스: locale 필터링 최적화\n index('cms_label_values_locale_idx').on(table.locale),\n]);\n\n// 타입 추론\nexport type CmsLabelValue = typeof cmsLabelValues.$inferSelect;\nexport type NewCmsLabelValue = typeof cmsLabelValues.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 텍스트 값 저장\n * await db.insert(cmsLabelValues).values({\n * labelId: 1,\n * version: 1,\n * locale: 'ko',\n * breakpoint: null,\n * value: {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * }\n * });\n *\n * // 반응형 이미지 저장 (모바일용)\n * await db.insert(cmsLabelValues).values({\n * labelId: 2,\n * version: 1,\n * locale: 'ko',\n * breakpoint: 'sm',\n * value: {\n * type: 'image',\n * url: '/uploads/hero-mobile.jpg',\n * alt: 'Hero Image',\n * width: 640,\n * height: 480\n * }\n * });\n *\n * // 특정 버전의 한국어 값 조회\n * const values = await db.select()\n * .from(cmsLabelValues)\n * .where(and(\n * eq(cmsLabelValues.labelId, 1),\n * eq(cmsLabelValues.version, 2),\n * eq(cmsLabelValues.locale, 'ko')\n * ));\n *\n * // Object 타입 값 저장 (재귀 구조)\n * await db.insert(cmsLabelValues).values({\n * labelId: 3,\n * version: 1,\n * locale: 'ko',\n * value: {\n * type: 'object',\n * fields: {\n * title: { type: 'text', content: '특징 1' },\n * icon: { type: 'image', url: '/icons/feature1.svg', alt: 'Icon' },\n * description: { type: 'text', content: '상세 설명...' }\n * }\n * }\n * });\n */","/**\n * CMS Published Cache Entity\n *\n * 발행된 콘텐츠를 섹션+언어 단위로 캐싱합니다.\n * - 초고속 읽기 성능 (5ms)\n * - 단일 쿼리로 섹션 전체 로드\n * - JSONB로 즉시 사용 가능한 데이터\n *\n * 성능 비교:\n * - 정규화 테이블 JOIN: 87ms\n * - 캐시 테이블: 5ms (17배 빠름!)\n */\n\nimport { text, integer, index, unique } from 'drizzle-orm/pg-core';\nimport { id, publishingFields, typedJsonb } from \"@spfn/core/db\";\nimport { cmsSchema } from './cms-schema';\n\n// Create isolated schema for @spfn/cms\n// Schema imported from cms-schema.ts\nexport const cmsPublishedCache = cmsSchema.table('published_cache', {\n // Primary Key\n id: id(),\n\n // 섹션 (페이지 단위)\n section: text('section').notNull(),\n // \"home\" | \"why-futureplay\" | \"team\" | \"our-companies\" | \"apply\"\n\n // 언어\n locale: text('locale').notNull(),\n // \"ko\" | \"en\" | \"ja\"\n\n // 캐시된 콘텐츠 (JSONB)\n content: typedJsonb<Record<string, any>>('content').notNull(),\n // Record<string, LabelValue>\n // {\n // \"home.hero.title\": { type: \"text\", content: \"...\" },\n // \"home.hero.image\": { type: \"image\", url: \"...\", alt: \"...\" },\n // ...\n // }\n\n // 발행 정보\n ...publishingFields(),\n\n // 캐시 버전 (클라이언트 캐싱용)\n version: integer('version').notNull().default(1),\n}, (table) => [\n // UNIQUE 제약: section + locale 조합은 유일\n unique('cms_published_cache_unique').on(table.section, table.locale),\n\n // 인덱스: section으로 조회 최적화\n index('cms_published_cache_section_idx').on(table.section),\n]);\n\n// 타입 추론\nexport type CmsPublishedCache = typeof cmsPublishedCache.$inferSelect;\nexport type NewCmsPublishedCache = typeof cmsPublishedCache.$inferInsert;\n\n/**\n * 사용 예시:\n *\n * // 캐시 생성/업데이트 (UPSERT)\n * await db.insert(cmsPublishedCache)\n * .values({\n * section: 'home',\n * locale: 'ko',\n * content: {\n * 'home.hero.title': {\n * type: 'text',\n * content: '미래를 만드는 기업'\n * },\n * 'home.hero.image': {\n * type: 'image',\n * url: '/uploads/hero.jpg',\n * alt: 'Hero',\n * width: 1920,\n * height: 1080\n * }\n * },\n * publishedAt: new Date(),\n * publishedBy: 'admin@futureplay.com'\n * })\n * .onConflictDoUpdate({\n * target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n * set: {\n * content: sql`EXCLUDED.content`,\n * publishedAt: sql`EXCLUDED.published_at`,\n * publishedBy: sql`EXCLUDED.published_by`,\n * version: sql`${cmsPublishedCache.version} + 1`\n * }\n * });\n *\n * // 캐시 조회 (초고속!)\n * const cache = await db.select()\n * .from(cmsPublishedCache)\n * .where(and(\n * eq(cmsPublishedCache.section, 'home'),\n * eq(cmsPublishedCache.locale, 'ko')\n * ))\n * .limit(1);\n *\n * const labels = cache[0].content; // 즉시 사용 가능!\n *\n * // 섹션의 모든 언어 캐시 조회\n * const allLocales = await db.select()\n * .from(cmsPublishedCache)\n * .where(eq(cmsPublishedCache.section, 'home'));\n *\n * // 오래된 캐시 감지\n * const stale = await db.select()\n * .from(cmsPublishedCache)\n * .where(lt(\n * cmsPublishedCache.publishedAt,\n * new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)\n * ));\n */","/**\n * CMS Label Values Repository\n *\n * 라벨 값 관리를 위한 Repository\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, SQL, isNull, gte, lte, inArray } from 'drizzle-orm';\nimport { cmsLabelValues, type CmsLabelValue, type NewCmsLabelValue } from '../entities';\n\n/**\n * 버전 히스토리 타입\n */\nexport interface VersionHistory\n{\n version: number;\n publishedAt: string;\n publishedBy: null;\n notes: null;\n values: Array<{\n id: number;\n locale: string;\n breakpoint: string | null;\n value: any;\n createdAt: string;\n }>;\n}\n\n/**\n * CMS Label Values Repository 클래스\n */\nexport class CmsLabelValuesRepository extends BaseRepository\n{\n /**\n * 특정 라벨의 특정 버전 값들 조회\n * Read replica 사용\n */\n async findByLabelIdAndVersion(\n labelId: number,\n version: number,\n options?: {\n locale?: string;\n breakpoint?: string | null;\n }\n ): Promise<CmsLabelValue[]>\n {\n const { locale, breakpoint } = options || {};\n\n const conditions: SQL[] = [\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n ];\n\n if (locale)\n {\n conditions.push(eq(cmsLabelValues.locale, locale));\n }\n\n if (breakpoint !== undefined)\n {\n conditions.push(\n breakpoint === null\n ? isNull(cmsLabelValues.breakpoint)\n : eq(cmsLabelValues.breakpoint, breakpoint)\n );\n }\n\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(and(...conditions));\n }\n\n /**\n * 값 저장 (upsert)\n * - version: null → Draft 저장 (덮어쓰기)\n * - version: number → Published 버전 생성 (불변)\n * Write primary 사용\n */\n async upsert(data: NewCmsLabelValue & { labelId: number }): Promise<CmsLabelValue>\n {\n // 기존 값이 있는지 확인\n const versionCondition = data.version === null || data.version === undefined\n ? isNull(cmsLabelValues.version)\n : eq(cmsLabelValues.version, data.version as number);\n\n const existingResult = await this.db\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, data.labelId),\n versionCondition,\n eq(cmsLabelValues.locale, data.locale || 'ko'),\n data.breakpoint\n ? eq(cmsLabelValues.breakpoint, data.breakpoint)\n : isNull(cmsLabelValues.breakpoint)\n )\n )\n .limit(1);\n\n const existing = existingResult[0];\n\n if (existing)\n {\n // UPDATE (only for drafts with version: null)\n if (data.version === null || data.version === undefined)\n {\n const updated = await this.db\n .update(cmsLabelValues)\n .set({ value: data.value })\n .where(eq(cmsLabelValues.id, existing.id))\n .returning();\n\n return updated[0];\n }\n else\n {\n // Published versions are immutable - this shouldn't happen\n throw new Error(`Published version ${data.version} already exists and cannot be overwritten`);\n }\n }\n else\n {\n // INSERT (both draft and new published versions)\n const inserted = await this.db\n .insert(cmsLabelValues)\n .values(data)\n .returning();\n\n return inserted[0];\n }\n }\n\n /**\n * Draft 값들 조회 (version = null)\n * Read replica 사용\n */\n async findDraftsByLabelId(labelId: number): Promise<CmsLabelValue[]>\n {\n return this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n isNull(cmsLabelValues.version)\n )\n );\n }\n\n /**\n * 여러 값 일괄 저장\n * Write primary 사용\n */\n async upsertMany(values: (NewCmsLabelValue & { labelId: number })[]): Promise<CmsLabelValue[]>\n {\n const results = [];\n for (const value of values)\n {\n const result = await this.upsert(value);\n results.push(result);\n }\n return results;\n }\n\n /**\n * 특정 버전의 모든 값 삭제\n * Write primary 사용\n */\n async deleteByVersion(labelId: number, version: number): Promise<CmsLabelValue[]>\n {\n return this.db\n .delete(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n eq(cmsLabelValues.version, version)\n )\n )\n .returning();\n }\n\n /**\n * 여러 라벨의 publishedVersion 값들을 한 번에 조회 (N+1 문제 해결)\n * Read replica 사용\n *\n * @param labelVersions - { labelId, version } 배열\n * @returns labelId를 키로 하는 Map<labelId, CmsLabelValue[]>\n *\n * @example\n * ```typescript\n * const result = await findByLabelVersions([\n * { labelId: 1, version: 5 },\n * { labelId: 2, version: 3 }\n * ]);\n * // result.get(1) -> label 1의 version 5 값들\n * // result.get(2) -> label 2의 version 3 값들\n * ```\n */\n async findByLabelVersions(\n labelVersions: Array<{ labelId: number; version: number }>\n ): Promise<Map<number, CmsLabelValue[]>>\n {\n if (labelVersions.length === 0)\n {\n return new Map();\n }\n\n // 모든 label의 publishedVersion 값들을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n inArray(\n cmsLabelValues.labelId,\n labelVersions.map(lv => lv.labelId)\n )\n )\n );\n\n // labelId와 version으로 필터링하여 Map 생성\n const versionMap = new Map(labelVersions.map(lv => [lv.labelId, lv.version]));\n const resultMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n const expectedVersion = versionMap.get(value.labelId);\n\n // 해당 labelId의 version이 일치하는 경우만 포함\n if (expectedVersion !== undefined && value.version === expectedVersion)\n {\n if (!resultMap.has(value.labelId))\n {\n resultMap.set(value.labelId, []);\n }\n resultMap.get(value.labelId)!.push(value);\n }\n }\n\n return resultMap;\n }\n\n /**\n * 라벨의 버전 히스토리 조회 (1 ~ maxVersion)\n * 한 번의 쿼리로 모든 버전을 조회하고 version별로 그룹화\n * Read replica 사용\n */\n async findVersionHistoryByLabelId(\n labelId: number,\n maxVersion: number\n ): Promise<VersionHistory[]>\n {\n // 모든 버전의 값을 한 번에 조회\n const allValues = await this.readDb\n .select()\n .from(cmsLabelValues)\n .where(\n and(\n eq(cmsLabelValues.labelId, labelId),\n gte(cmsLabelValues.version, 1),\n lte(cmsLabelValues.version, maxVersion)\n )\n )\n .orderBy(cmsLabelValues.version, cmsLabelValues.locale);\n\n // version별로 그룹화\n const versionMap = new Map<number, CmsLabelValue[]>();\n\n for (const value of allValues)\n {\n if (value.version === null) continue; // null 버전은 제외\n\n if (!versionMap.has(value.version))\n {\n versionMap.set(value.version, []);\n }\n versionMap.get(value.version)!.push(value);\n }\n\n // VersionHistory 형식으로 변환\n const versions: VersionHistory[] = [];\n\n for (let version = 1; version <= maxVersion; version++)\n {\n const values = versionMap.get(version);\n\n if (values && values.length > 0)\n {\n versions.push({\n version,\n publishedAt: values[0].createdAt.toISOString(),\n publishedBy: null, // label_values에는 publishedBy 정보가 없음\n notes: null, // label_values에는 notes 정보가 없음\n values: values.map(v => ({\n id: v.id,\n locale: v.locale,\n breakpoint: v.breakpoint,\n value: v.value,\n createdAt: v.createdAt.toISOString()\n }))\n });\n }\n }\n\n // 버전 내림차순 정렬 (최신 버전이 먼저)\n versions.sort((a, b) => b.version - a.version);\n\n return versions;\n }\n}\n\n// Default instance export\nexport const cmsLabelValuesRepository = new CmsLabelValuesRepository();","/**\n * CMS Published Cache Repository\n *\n * 발행된 콘텐츠 캐시 관리 (초고속 조회)\n * BaseRepository를 상속받아 자동 트랜잭션 컨텍스트 지원 및 Read/Write 분리\n */\n\nimport { BaseRepository } from '@spfn/core/db';\nimport { eq, and, sql, inArray } from 'drizzle-orm';\nimport { cmsPublishedCache, type CmsPublishedCache, type NewCmsPublishedCache } from '../entities';\n\n/**\n * CMS Published Cache Repository 클래스\n */\nexport class CmsPublishedCacheRepository extends BaseRepository\n{\n /**\n * 섹션 + 언어로 발행된 캐시 조회\n * Read replica 사용\n */\n async findBySection(section: string, locale: string = 'en'): Promise<CmsPublishedCache | null>\n {\n const result = await this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n )\n .limit(1);\n\n return result[0] ?? null;\n }\n\n /**\n * 캐시 생성 또는 업데이트 (UPSERT)\n * Write primary 사용\n */\n async upsert(data: NewCmsPublishedCache): Promise<CmsPublishedCache>\n {\n const result = await this.db\n .insert(cmsPublishedCache)\n .values(data)\n .onConflictDoUpdate({\n target: [cmsPublishedCache.section, cmsPublishedCache.locale],\n set: {\n content: data.content,\n publishedAt: data.publishedAt,\n publishedBy: data.publishedBy,\n version: sql`${cmsPublishedCache.version} + 1`, // 버전 증가로 클라이언트 캐시 무효화\n }\n })\n .returning();\n\n return result[0];\n }\n\n /**\n * 여러 섹션의 캐시를 한 번에 조회 (N+1 방지)\n * Read replica 사용\n */\n async findBySections(sections: string[], locale: string = 'en'): Promise<CmsPublishedCache[]>\n {\n if (sections.length === 0)\n {\n return [];\n }\n\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(\n and(\n inArray(cmsPublishedCache.section, sections),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n\n /**\n * 섹션별 모든 언어 캐시 조회\n * Read replica 사용\n */\n async findAllLanguages(section: string): Promise<CmsPublishedCache[]>\n {\n return this.readDb\n .select()\n .from(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n\n /**\n * 캐시 삭제\n * Write primary 사용\n */\n async deleteBySection(section: string, locale?: string): Promise<void>\n {\n if (locale)\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(\n and(\n eq(cmsPublishedCache.section, section),\n eq(cmsPublishedCache.locale, locale)\n )\n );\n }\n else\n {\n await this.db\n .delete(cmsPublishedCache)\n .where(eq(cmsPublishedCache.section, section));\n }\n }\n}\n\n// Default instance export\nexport const cmsPublishedCacheRepository = new CmsPublishedCacheRepository();","/**\n * CMS Label Synchronization Service\n *\n * Synchronizes labels defined in code with database\n */\n\nimport { isEqual } from 'lodash-es';\nimport { type SyncOptions, type SyncResult } from '../../lib/types';\nimport { type FlatLabel, flattenLabels } from '../../lib/helpers';\nimport { cmsLabelsRepository } from '../repositories';\nimport { type NewCmsLabel } from '../entities';\n\n/**\n * Compare current DB labels with new labels\n *\n * @param dbLabels - Labels currently in database\n * @param codeLabels - Labels from code (flattened)\n * @returns Comparison result with added/removed/updated labels\n */\nfunction compareLabels(dbLabels: FlatLabel, codeLabels: FlatLabel): SyncResult\n{\n const added: string[] = [];\n const removed: string[] = [];\n const updated: string[] = [];\n const unchanged: string[] = [];\n\n const dbKeys = Object.keys(dbLabels);\n const codeKeys = Object.keys(codeLabels);\n\n // Check for added and updated labels\n for (const key of codeKeys)\n {\n if (!(key in dbLabels))\n {\n // New label\n added.push(key);\n }\n else\n {\n // Check if values changed (deep equality check for nested objects)\n const dbValue = dbLabels[key];\n const codeValue = codeLabels[key];\n\n if (!isEqual(dbValue, codeValue))\n {\n updated.push(key);\n }\n else\n {\n unchanged.push(key);\n }\n }\n }\n\n // Check for removed labels\n for (const key of dbKeys)\n {\n if (!(key in codeLabels))\n {\n removed.push(key);\n }\n }\n\n return {\n added,\n removed,\n updated,\n unchanged,\n };\n}\n\n/**\n * Sync labels with database\n *\n * @param labels - Single label definition or array of label definitions\n * @param options - Sync options\n * @returns Sync result\n *\n * @example\n * ```typescript\n * // Single definition\n * await syncLabels(labelsDefinition);\n *\n * // Multiple definitions\n * await syncLabels([homeLabels, aboutLabels, commonLabels]);\n * ```\n */\nexport async function syncLabels<T extends Record<string, any>>(\n labels: T | T[],\n options?: SyncOptions\n): Promise<SyncResult>\n{\n const { removeOrphaned = false, dryRun = false } = options || {};\n\n // 1. Merge multiple label definitions into one (if array provided)\n const mergedLabels = Array.isArray(labels)\n ? Object.assign({}, ...labels)\n : labels;\n\n // 2. Flatten code labels\n const codeLabels = flattenLabels(mergedLabels);\n\n // 3. Fetch current labels from DB\n const dbLabels = await cmsLabelsRepository.findMany();\n const dbLabelMap: FlatLabel = {};\n\n for (const label of dbLabels)\n {\n if (label.defaultValue)\n {\n dbLabelMap[label.key] = label.defaultValue as Record<string, string>;\n }\n }\n\n // 4. Compare changes\n const result = compareLabels(dbLabelMap, codeLabels);\n\n // 5. Return result if dry run\n if (dryRun)\n {\n return result;\n }\n\n // 6. Create new labels\n if (result.added.length > 0)\n {\n const toCreate: NewCmsLabel[] = result.added.map(key => ({\n key,\n section: extractSection(key),\n type: 'text',\n defaultValue: codeLabels[key],\n }));\n\n await cmsLabelsRepository.bulkCreate(toCreate);\n }\n\n // 7. Update changed labels\n if (result.updated.length > 0)\n {\n const updates = result.updated.map(key => ({\n key,\n data: {\n defaultValue: codeLabels[key],\n },\n }));\n\n await cmsLabelsRepository.bulkUpdateByKeys(updates);\n }\n\n // 8. Remove deleted labels (only if option is true)\n if (removeOrphaned && result.removed.length > 0)\n {\n await cmsLabelsRepository.bulkDeleteByKeys(result.removed);\n }\n\n return result;\n}\n\n/**\n * Extract section from key\n * Example: \"home.hero.title\" -> \"home\"\n */\nfunction extractSection(key: string): string\n{\n const parts = key.split('.');\n return parts[0] || key;\n}","/**\n * CMS Helper Functions\n */\n\nexport type FlatLabel = Record<string, Record<string, string>>;\n\n/**\n * Flatten nested label structure into dot notation\n *\n * @param labels - Nested label object\n * @param prefix - Key prefix for recursion\n * @returns Flattened label structure\n *\n * @example\n * ```typescript\n * const nested = {\n * home: {\n * hero: {\n * title: { en: \"Welcome\", ko: \"환영합니다\" }\n * }\n * }\n * };\n *\n * const flat = flattenLabels(nested);\n * // { \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" } }\n * ```\n */\nexport function flattenLabels<T extends Record<string, any>>(labels: T, prefix = ''): FlatLabel\n{\n const result: FlatLabel = {};\n\n if (!labels || typeof labels !== 'object')\n {\n return result;\n }\n\n const obj = labels as Record<string, unknown>;\n\n for (const [key, value] of Object.entries(obj))\n {\n const newKey = prefix ? `${prefix}.${key}` : key;\n\n if (!value || typeof value !== 'object')\n {\n continue;\n }\n\n const valueObj = value as Record<string, unknown>;\n\n // Check if this is a leaf node (locale values: { en: \"...\", ko: \"...\" })\n const isLeaf = Object.values(valueObj).every(v => typeof v === 'string');\n\n if (isLeaf)\n {\n result[newKey] = valueObj as Record<string, string>;\n }\n else\n {\n // Recursively flatten nested structure\n Object.assign(result, flattenLabels(value, newKey));\n }\n }\n\n return result;\n}\n\n/**\n * Set a value in nested object using dot notation path\n *\n * @param target - Target object to modify\n * @param path - Dot notation path (e.g., \"home.hero.title\")\n * @param value - Value to set\n *\n * @example\n * ```typescript\n * const obj = {};\n * setNestedValue(obj, \"home.hero.title\", \"Welcome\");\n * // obj = { home: { hero: { title: \"Welcome\" } } }\n * ```\n */\nexport function setNestedValue(target: any, path: string, value: any): void\n{\n const parts = path.split('.');\n let current = target;\n\n for (let i = 0; i < parts.length - 1; i++)\n {\n const part = parts[i];\n if (!current[part])\n {\n current[part] = {};\n }\n current = current[part];\n }\n\n // Set the leaf value\n const lastPart = parts[parts.length - 1];\n current[lastPart] = value;\n}\n\n/**\n * Unflatten dot notation keys back to nested structure\n *\n * @param flat - Flattened label object\n * @returns Nested label structure\n *\n * @example\n * ```typescript\n * const flat = {\n * \"home.hero.title\": { en: \"Welcome\", ko: \"환영합니다\" },\n * \"home.hero.subtitle\": { en: \"Subtitle\", ko: \"부제목\" }\n * };\n *\n * const nested = unflattenLabels(flat);\n * // {\n * // home: {\n * // hero: {\n * // title: { en: \"Welcome\", ko: \"환영합니다\" },\n * // subtitle: { en: \"Subtitle\", ko: \"부제목\" }\n * // }\n * // }\n * // }\n * ```\n */\nexport function unflattenLabels(flat: FlatLabel): Record<string, any>\n{\n const result: Record<string, any> = {};\n\n for (const [key, value] of Object.entries(flat))\n {\n setNestedValue(result, key, value);\n }\n\n return result;\n}"],"mappings":"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","names":["integer","text","index","id","typedJsonb","id","integer","text","typedJsonb","index","text","integer","index","unique","id","typedJsonb","id","text","typedJsonb","integer","unique","index","id","BaseRepository","eq","inArray","BaseRepository","eq","inArray","BaseRepository","eq","and","inArray","BaseRepository","and","eq","inArray"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/cms",
3
- "version": "0.2.0-beta.3",
3
+ "version": "0.2.0-beta.5",
4
4
  "description": "SPFN CMS - Content Management System with type-safe labels and Next.js integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -66,19 +66,19 @@
66
66
  "setupMessage": " 📚 Next steps:\n 1. Import CMS components: import { useLabels } from '@spfn/cms'\n 2. View labels in Drizzle Studio: pnpm spfn db studio\n 3. CMS API available at: http://localhost:8790/_cms\n 4. Learn more: https://github.com/spfnio/spfn"
67
67
  },
68
68
  "peerDependencies": {
69
- "drizzle-orm": "^0.44.7",
69
+ "drizzle-orm": ">=0.44.7",
70
70
  "next": "^15.0.0 || ^16.0.0",
71
- "react": "^18.0.0 || ^19.0.0",
72
- "@spfn/core": "0.2.0-beta.1"
71
+ "react": "^18.0.0 || ^19.0.0"
73
72
  },
74
73
  "dependencies": {
75
74
  "@sinclair/typebox": "^0.34.0",
76
75
  "jiti": "^2.6.1",
77
- "lodash": "^4.17.21",
78
- "zustand": "^5.0.8"
76
+ "lodash-es": "^4.17.21",
77
+ "zustand": "^5.0.8",
78
+ "@spfn/core": "0.2.0-beta.5"
79
79
  },
80
80
  "devDependencies": {
81
- "@types/lodash": "^4.17.21",
81
+ "@types/lodash-es": "^4.17.12",
82
82
  "@types/node": "^20.11.0",
83
83
  "@types/react": "^19",
84
84
  "@vitest/coverage-v8": "^4.0.6",
@@ -91,7 +91,7 @@
91
91
  "tsx": "^4.20.6",
92
92
  "typescript": "^5.3.3",
93
93
  "vitest": "^4.0.6",
94
- "spfn": "0.2.0-beta.1"
94
+ "spfn": "0.2.0-beta.4"
95
95
  },
96
96
  "scripts": {
97
97
  "build": "pnpm check:circular && tsup",