admin_resource_service.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "net/http"
  6. "strings"
  7. "cmr-backend/internal/apperr"
  8. "cmr-backend/internal/platform/security"
  9. "cmr-backend/internal/store/postgres"
  10. )
  11. type AdminResourceService struct {
  12. store *postgres.Store
  13. }
  14. type AdminMapSummary struct {
  15. ID string `json:"id"`
  16. Code string `json:"code"`
  17. Name string `json:"name"`
  18. Status string `json:"status"`
  19. Description *string `json:"description,omitempty"`
  20. CurrentVersionID *string `json:"currentVersionId,omitempty"`
  21. CurrentVersion *AdminMapVersionBrief `json:"currentVersion,omitempty"`
  22. }
  23. type AdminMapVersionBrief struct {
  24. ID string `json:"id"`
  25. VersionCode string `json:"versionCode"`
  26. Status string `json:"status"`
  27. }
  28. type AdminMapVersion struct {
  29. ID string `json:"id"`
  30. VersionCode string `json:"versionCode"`
  31. Status string `json:"status"`
  32. MapmetaURL string `json:"mapmetaUrl"`
  33. TilesRootURL string `json:"tilesRootUrl"`
  34. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  35. Bounds map[string]any `json:"bounds,omitempty"`
  36. Metadata map[string]any `json:"metadata,omitempty"`
  37. }
  38. type AdminMapDetail struct {
  39. Map AdminMapSummary `json:"map"`
  40. Versions []AdminMapVersion `json:"versions"`
  41. }
  42. type CreateAdminMapInput struct {
  43. Code string `json:"code"`
  44. Name string `json:"name"`
  45. Status string `json:"status"`
  46. Description *string `json:"description,omitempty"`
  47. }
  48. type CreateAdminMapVersionInput struct {
  49. VersionCode string `json:"versionCode"`
  50. Status string `json:"status"`
  51. MapmetaURL string `json:"mapmetaUrl"`
  52. TilesRootURL string `json:"tilesRootUrl"`
  53. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  54. Bounds map[string]any `json:"bounds,omitempty"`
  55. Metadata map[string]any `json:"metadata,omitempty"`
  56. SetAsCurrent bool `json:"setAsCurrent"`
  57. }
  58. type AdminPlayfieldSummary struct {
  59. ID string `json:"id"`
  60. Code string `json:"code"`
  61. Name string `json:"name"`
  62. Kind string `json:"kind"`
  63. Status string `json:"status"`
  64. Description *string `json:"description,omitempty"`
  65. CurrentVersionID *string `json:"currentVersionId,omitempty"`
  66. CurrentVersion *AdminPlayfieldVersionBrief `json:"currentVersion,omitempty"`
  67. }
  68. type AdminPlayfieldVersionBrief struct {
  69. ID string `json:"id"`
  70. VersionCode string `json:"versionCode"`
  71. Status string `json:"status"`
  72. SourceType string `json:"sourceType"`
  73. }
  74. type AdminPlayfieldVersion struct {
  75. ID string `json:"id"`
  76. VersionCode string `json:"versionCode"`
  77. Status string `json:"status"`
  78. SourceType string `json:"sourceType"`
  79. SourceURL string `json:"sourceUrl"`
  80. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  81. ControlCount *int `json:"controlCount,omitempty"`
  82. Bounds map[string]any `json:"bounds,omitempty"`
  83. Metadata map[string]any `json:"metadata,omitempty"`
  84. }
  85. type AdminPlayfieldDetail struct {
  86. Playfield AdminPlayfieldSummary `json:"playfield"`
  87. Versions []AdminPlayfieldVersion `json:"versions"`
  88. }
  89. type CreateAdminPlayfieldInput struct {
  90. Code string `json:"code"`
  91. Name string `json:"name"`
  92. Kind string `json:"kind"`
  93. Status string `json:"status"`
  94. Description *string `json:"description,omitempty"`
  95. }
  96. type CreateAdminPlayfieldVersionInput struct {
  97. VersionCode string `json:"versionCode"`
  98. Status string `json:"status"`
  99. SourceType string `json:"sourceType"`
  100. SourceURL string `json:"sourceUrl"`
  101. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  102. ControlCount *int `json:"controlCount,omitempty"`
  103. Bounds map[string]any `json:"bounds,omitempty"`
  104. Metadata map[string]any `json:"metadata,omitempty"`
  105. SetAsCurrent bool `json:"setAsCurrent"`
  106. }
  107. type AdminResourcePackSummary struct {
  108. ID string `json:"id"`
  109. Code string `json:"code"`
  110. Name string `json:"name"`
  111. Status string `json:"status"`
  112. Description *string `json:"description,omitempty"`
  113. CurrentVersionID *string `json:"currentVersionId,omitempty"`
  114. CurrentVersion *AdminResourcePackVersionBrief `json:"currentVersion,omitempty"`
  115. }
  116. type AdminResourcePackVersionBrief struct {
  117. ID string `json:"id"`
  118. VersionCode string `json:"versionCode"`
  119. Status string `json:"status"`
  120. }
  121. type AdminResourcePackVersion struct {
  122. ID string `json:"id"`
  123. VersionCode string `json:"versionCode"`
  124. Status string `json:"status"`
  125. ContentEntryURL *string `json:"contentEntryUrl,omitempty"`
  126. AudioRootURL *string `json:"audioRootUrl,omitempty"`
  127. ThemeProfileCode *string `json:"themeProfileCode,omitempty"`
  128. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  129. Metadata map[string]any `json:"metadata,omitempty"`
  130. }
  131. type AdminResourcePackDetail struct {
  132. ResourcePack AdminResourcePackSummary `json:"resourcePack"`
  133. Versions []AdminResourcePackVersion `json:"versions"`
  134. }
  135. type CreateAdminResourcePackInput struct {
  136. Code string `json:"code"`
  137. Name string `json:"name"`
  138. Status string `json:"status"`
  139. Description *string `json:"description,omitempty"`
  140. }
  141. type CreateAdminResourcePackVersionInput struct {
  142. VersionCode string `json:"versionCode"`
  143. Status string `json:"status"`
  144. ContentEntryURL *string `json:"contentEntryUrl,omitempty"`
  145. AudioRootURL *string `json:"audioRootUrl,omitempty"`
  146. ThemeProfileCode *string `json:"themeProfileCode,omitempty"`
  147. PublishedAssetRoot *string `json:"publishedAssetRoot,omitempty"`
  148. Metadata map[string]any `json:"metadata,omitempty"`
  149. SetAsCurrent bool `json:"setAsCurrent"`
  150. }
  151. func NewAdminResourceService(store *postgres.Store) *AdminResourceService {
  152. return &AdminResourceService{store: store}
  153. }
  154. func (s *AdminResourceService) ListMaps(ctx context.Context, limit int) ([]AdminMapSummary, error) {
  155. items, err := s.store.ListResourceMaps(ctx, limit)
  156. if err != nil {
  157. return nil, err
  158. }
  159. results := make([]AdminMapSummary, 0, len(items))
  160. for _, item := range items {
  161. results = append(results, AdminMapSummary{
  162. ID: item.PublicID,
  163. Code: item.Code,
  164. Name: item.Name,
  165. Status: item.Status,
  166. Description: item.Description,
  167. CurrentVersionID: item.CurrentVersionID,
  168. })
  169. }
  170. return results, nil
  171. }
  172. func (s *AdminResourceService) CreateMap(ctx context.Context, input CreateAdminMapInput) (*AdminMapSummary, error) {
  173. input.Code = strings.TrimSpace(input.Code)
  174. input.Name = strings.TrimSpace(input.Name)
  175. status := normalizeCatalogStatus(input.Status)
  176. if input.Code == "" || input.Name == "" {
  177. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
  178. }
  179. publicID, err := security.GeneratePublicID("map")
  180. if err != nil {
  181. return nil, err
  182. }
  183. tx, err := s.store.Begin(ctx)
  184. if err != nil {
  185. return nil, err
  186. }
  187. defer tx.Rollback(ctx)
  188. item, err := s.store.CreateResourceMap(ctx, tx, postgres.CreateResourceMapParams{
  189. PublicID: publicID,
  190. Code: input.Code,
  191. Name: input.Name,
  192. Status: status,
  193. Description: trimStringPtr(input.Description),
  194. })
  195. if err != nil {
  196. return nil, err
  197. }
  198. if err := tx.Commit(ctx); err != nil {
  199. return nil, err
  200. }
  201. return &AdminMapSummary{
  202. ID: item.PublicID,
  203. Code: item.Code,
  204. Name: item.Name,
  205. Status: item.Status,
  206. Description: item.Description,
  207. CurrentVersionID: item.CurrentVersionID,
  208. }, nil
  209. }
  210. func (s *AdminResourceService) GetMapDetail(ctx context.Context, mapPublicID string) (*AdminMapDetail, error) {
  211. item, err := s.store.GetResourceMapByPublicID(ctx, strings.TrimSpace(mapPublicID))
  212. if err != nil {
  213. return nil, err
  214. }
  215. if item == nil {
  216. return nil, apperr.New(http.StatusNotFound, "map_not_found", "map not found")
  217. }
  218. versions, err := s.store.ListResourceMapVersions(ctx, item.ID)
  219. if err != nil {
  220. return nil, err
  221. }
  222. result := &AdminMapDetail{
  223. Map: AdminMapSummary{
  224. ID: item.PublicID,
  225. Code: item.Code,
  226. Name: item.Name,
  227. Status: item.Status,
  228. Description: item.Description,
  229. CurrentVersionID: item.CurrentVersionID,
  230. },
  231. Versions: make([]AdminMapVersion, 0, len(versions)),
  232. }
  233. for _, version := range versions {
  234. view := AdminMapVersion{
  235. ID: version.PublicID,
  236. VersionCode: version.VersionCode,
  237. Status: version.Status,
  238. MapmetaURL: version.MapmetaURL,
  239. TilesRootURL: version.TilesRootURL,
  240. PublishedAssetRoot: version.PublishedAssetRoot,
  241. Bounds: decodeJSONMap(version.BoundsJSON),
  242. Metadata: decodeJSONMap(version.MetadataJSON),
  243. }
  244. result.Versions = append(result.Versions, view)
  245. if item.CurrentVersionID != nil && *item.CurrentVersionID == version.ID {
  246. result.Map.CurrentVersion = &AdminMapVersionBrief{
  247. ID: version.PublicID,
  248. VersionCode: version.VersionCode,
  249. Status: version.Status,
  250. }
  251. result.Map.CurrentVersionID = &view.ID
  252. }
  253. }
  254. return result, nil
  255. }
  256. func (s *AdminResourceService) CreateMapVersion(ctx context.Context, mapPublicID string, input CreateAdminMapVersionInput) (*AdminMapVersion, error) {
  257. mapItem, err := s.store.GetResourceMapByPublicID(ctx, strings.TrimSpace(mapPublicID))
  258. if err != nil {
  259. return nil, err
  260. }
  261. if mapItem == nil {
  262. return nil, apperr.New(http.StatusNotFound, "map_not_found", "map not found")
  263. }
  264. input.VersionCode = strings.TrimSpace(input.VersionCode)
  265. input.MapmetaURL = strings.TrimSpace(input.MapmetaURL)
  266. input.TilesRootURL = strings.TrimSpace(input.TilesRootURL)
  267. if input.VersionCode == "" || input.MapmetaURL == "" || input.TilesRootURL == "" {
  268. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode, mapmetaUrl and tilesRootUrl are required")
  269. }
  270. publicID, err := security.GeneratePublicID("mapv")
  271. if err != nil {
  272. return nil, err
  273. }
  274. tx, err := s.store.Begin(ctx)
  275. if err != nil {
  276. return nil, err
  277. }
  278. defer tx.Rollback(ctx)
  279. version, err := s.store.CreateResourceMapVersion(ctx, tx, postgres.CreateResourceMapVersionParams{
  280. PublicID: publicID,
  281. MapID: mapItem.ID,
  282. VersionCode: input.VersionCode,
  283. Status: normalizeVersionStatus(input.Status),
  284. MapmetaURL: input.MapmetaURL,
  285. TilesRootURL: input.TilesRootURL,
  286. PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
  287. BoundsJSON: input.Bounds,
  288. MetadataJSON: input.Metadata,
  289. })
  290. if err != nil {
  291. return nil, err
  292. }
  293. if input.SetAsCurrent {
  294. if err := s.store.SetResourceMapCurrentVersion(ctx, tx, mapItem.ID, version.ID); err != nil {
  295. return nil, err
  296. }
  297. }
  298. if err := tx.Commit(ctx); err != nil {
  299. return nil, err
  300. }
  301. return &AdminMapVersion{
  302. ID: version.PublicID,
  303. VersionCode: version.VersionCode,
  304. Status: version.Status,
  305. MapmetaURL: version.MapmetaURL,
  306. TilesRootURL: version.TilesRootURL,
  307. PublishedAssetRoot: version.PublishedAssetRoot,
  308. Bounds: decodeJSONMap(version.BoundsJSON),
  309. Metadata: decodeJSONMap(version.MetadataJSON),
  310. }, nil
  311. }
  312. func (s *AdminResourceService) ListPlayfields(ctx context.Context, limit int) ([]AdminPlayfieldSummary, error) {
  313. items, err := s.store.ListResourcePlayfields(ctx, limit)
  314. if err != nil {
  315. return nil, err
  316. }
  317. results := make([]AdminPlayfieldSummary, 0, len(items))
  318. for _, item := range items {
  319. results = append(results, AdminPlayfieldSummary{
  320. ID: item.PublicID,
  321. Code: item.Code,
  322. Name: item.Name,
  323. Kind: item.Kind,
  324. Status: item.Status,
  325. Description: item.Description,
  326. CurrentVersionID: item.CurrentVersionID,
  327. })
  328. }
  329. return results, nil
  330. }
  331. func (s *AdminResourceService) CreatePlayfield(ctx context.Context, input CreateAdminPlayfieldInput) (*AdminPlayfieldSummary, error) {
  332. input.Code = strings.TrimSpace(input.Code)
  333. input.Name = strings.TrimSpace(input.Name)
  334. kind := strings.TrimSpace(input.Kind)
  335. if kind == "" {
  336. kind = "course"
  337. }
  338. if input.Code == "" || input.Name == "" {
  339. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
  340. }
  341. publicID, err := security.GeneratePublicID("pf")
  342. if err != nil {
  343. return nil, err
  344. }
  345. tx, err := s.store.Begin(ctx)
  346. if err != nil {
  347. return nil, err
  348. }
  349. defer tx.Rollback(ctx)
  350. item, err := s.store.CreateResourcePlayfield(ctx, tx, postgres.CreateResourcePlayfieldParams{
  351. PublicID: publicID,
  352. Code: input.Code,
  353. Name: input.Name,
  354. Kind: kind,
  355. Status: normalizeCatalogStatus(input.Status),
  356. Description: trimStringPtr(input.Description),
  357. })
  358. if err != nil {
  359. return nil, err
  360. }
  361. if err := tx.Commit(ctx); err != nil {
  362. return nil, err
  363. }
  364. return &AdminPlayfieldSummary{
  365. ID: item.PublicID,
  366. Code: item.Code,
  367. Name: item.Name,
  368. Kind: item.Kind,
  369. Status: item.Status,
  370. Description: item.Description,
  371. CurrentVersionID: item.CurrentVersionID,
  372. }, nil
  373. }
  374. func (s *AdminResourceService) GetPlayfieldDetail(ctx context.Context, publicID string) (*AdminPlayfieldDetail, error) {
  375. item, err := s.store.GetResourcePlayfieldByPublicID(ctx, strings.TrimSpace(publicID))
  376. if err != nil {
  377. return nil, err
  378. }
  379. if item == nil {
  380. return nil, apperr.New(http.StatusNotFound, "playfield_not_found", "playfield not found")
  381. }
  382. versions, err := s.store.ListResourcePlayfieldVersions(ctx, item.ID)
  383. if err != nil {
  384. return nil, err
  385. }
  386. result := &AdminPlayfieldDetail{
  387. Playfield: AdminPlayfieldSummary{
  388. ID: item.PublicID,
  389. Code: item.Code,
  390. Name: item.Name,
  391. Kind: item.Kind,
  392. Status: item.Status,
  393. Description: item.Description,
  394. CurrentVersionID: item.CurrentVersionID,
  395. },
  396. Versions: make([]AdminPlayfieldVersion, 0, len(versions)),
  397. }
  398. for _, version := range versions {
  399. view := AdminPlayfieldVersion{
  400. ID: version.PublicID,
  401. VersionCode: version.VersionCode,
  402. Status: version.Status,
  403. SourceType: version.SourceType,
  404. SourceURL: version.SourceURL,
  405. PublishedAssetRoot: version.PublishedAssetRoot,
  406. ControlCount: version.ControlCount,
  407. Bounds: decodeJSONMap(version.BoundsJSON),
  408. Metadata: decodeJSONMap(version.MetadataJSON),
  409. }
  410. result.Versions = append(result.Versions, view)
  411. if item.CurrentVersionID != nil && *item.CurrentVersionID == version.ID {
  412. result.Playfield.CurrentVersion = &AdminPlayfieldVersionBrief{
  413. ID: version.PublicID,
  414. VersionCode: version.VersionCode,
  415. Status: version.Status,
  416. SourceType: version.SourceType,
  417. }
  418. result.Playfield.CurrentVersionID = &view.ID
  419. }
  420. }
  421. return result, nil
  422. }
  423. func (s *AdminResourceService) CreatePlayfieldVersion(ctx context.Context, publicID string, input CreateAdminPlayfieldVersionInput) (*AdminPlayfieldVersion, error) {
  424. item, err := s.store.GetResourcePlayfieldByPublicID(ctx, strings.TrimSpace(publicID))
  425. if err != nil {
  426. return nil, err
  427. }
  428. if item == nil {
  429. return nil, apperr.New(http.StatusNotFound, "playfield_not_found", "playfield not found")
  430. }
  431. input.VersionCode = strings.TrimSpace(input.VersionCode)
  432. input.SourceType = strings.TrimSpace(input.SourceType)
  433. input.SourceURL = strings.TrimSpace(input.SourceURL)
  434. if input.VersionCode == "" || input.SourceType == "" || input.SourceURL == "" {
  435. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode, sourceType and sourceUrl are required")
  436. }
  437. publicVersionID, err := security.GeneratePublicID("pfv")
  438. if err != nil {
  439. return nil, err
  440. }
  441. tx, err := s.store.Begin(ctx)
  442. if err != nil {
  443. return nil, err
  444. }
  445. defer tx.Rollback(ctx)
  446. version, err := s.store.CreateResourcePlayfieldVersion(ctx, tx, postgres.CreateResourcePlayfieldVersionParams{
  447. PublicID: publicVersionID,
  448. PlayfieldID: item.ID,
  449. VersionCode: input.VersionCode,
  450. Status: normalizeVersionStatus(input.Status),
  451. SourceType: input.SourceType,
  452. SourceURL: input.SourceURL,
  453. PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
  454. ControlCount: input.ControlCount,
  455. BoundsJSON: input.Bounds,
  456. MetadataJSON: input.Metadata,
  457. })
  458. if err != nil {
  459. return nil, err
  460. }
  461. if input.SetAsCurrent {
  462. if err := s.store.SetResourcePlayfieldCurrentVersion(ctx, tx, item.ID, version.ID); err != nil {
  463. return nil, err
  464. }
  465. }
  466. if err := tx.Commit(ctx); err != nil {
  467. return nil, err
  468. }
  469. return &AdminPlayfieldVersion{
  470. ID: version.PublicID,
  471. VersionCode: version.VersionCode,
  472. Status: version.Status,
  473. SourceType: version.SourceType,
  474. SourceURL: version.SourceURL,
  475. PublishedAssetRoot: version.PublishedAssetRoot,
  476. ControlCount: version.ControlCount,
  477. Bounds: decodeJSONMap(version.BoundsJSON),
  478. Metadata: decodeJSONMap(version.MetadataJSON),
  479. }, nil
  480. }
  481. func (s *AdminResourceService) ListResourcePacks(ctx context.Context, limit int) ([]AdminResourcePackSummary, error) {
  482. items, err := s.store.ListResourcePacks(ctx, limit)
  483. if err != nil {
  484. return nil, err
  485. }
  486. results := make([]AdminResourcePackSummary, 0, len(items))
  487. for _, item := range items {
  488. results = append(results, AdminResourcePackSummary{
  489. ID: item.PublicID,
  490. Code: item.Code,
  491. Name: item.Name,
  492. Status: item.Status,
  493. Description: item.Description,
  494. CurrentVersionID: item.CurrentVersionID,
  495. })
  496. }
  497. return results, nil
  498. }
  499. func (s *AdminResourceService) CreateResourcePack(ctx context.Context, input CreateAdminResourcePackInput) (*AdminResourcePackSummary, error) {
  500. input.Code = strings.TrimSpace(input.Code)
  501. input.Name = strings.TrimSpace(input.Name)
  502. if input.Code == "" || input.Name == "" {
  503. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "code and name are required")
  504. }
  505. publicID, err := security.GeneratePublicID("rp")
  506. if err != nil {
  507. return nil, err
  508. }
  509. tx, err := s.store.Begin(ctx)
  510. if err != nil {
  511. return nil, err
  512. }
  513. defer tx.Rollback(ctx)
  514. item, err := s.store.CreateResourcePack(ctx, tx, postgres.CreateResourcePackParams{
  515. PublicID: publicID,
  516. Code: input.Code,
  517. Name: input.Name,
  518. Status: normalizeCatalogStatus(input.Status),
  519. Description: trimStringPtr(input.Description),
  520. })
  521. if err != nil {
  522. return nil, err
  523. }
  524. if err := tx.Commit(ctx); err != nil {
  525. return nil, err
  526. }
  527. return &AdminResourcePackSummary{
  528. ID: item.PublicID,
  529. Code: item.Code,
  530. Name: item.Name,
  531. Status: item.Status,
  532. Description: item.Description,
  533. CurrentVersionID: item.CurrentVersionID,
  534. }, nil
  535. }
  536. func (s *AdminResourceService) GetResourcePackDetail(ctx context.Context, publicID string) (*AdminResourcePackDetail, error) {
  537. item, err := s.store.GetResourcePackByPublicID(ctx, strings.TrimSpace(publicID))
  538. if err != nil {
  539. return nil, err
  540. }
  541. if item == nil {
  542. return nil, apperr.New(http.StatusNotFound, "resource_pack_not_found", "resource pack not found")
  543. }
  544. versions, err := s.store.ListResourcePackVersions(ctx, item.ID)
  545. if err != nil {
  546. return nil, err
  547. }
  548. result := &AdminResourcePackDetail{
  549. ResourcePack: AdminResourcePackSummary{
  550. ID: item.PublicID,
  551. Code: item.Code,
  552. Name: item.Name,
  553. Status: item.Status,
  554. Description: item.Description,
  555. CurrentVersionID: item.CurrentVersionID,
  556. },
  557. Versions: make([]AdminResourcePackVersion, 0, len(versions)),
  558. }
  559. for _, version := range versions {
  560. view := AdminResourcePackVersion{
  561. ID: version.PublicID,
  562. VersionCode: version.VersionCode,
  563. Status: version.Status,
  564. ContentEntryURL: version.ContentEntryURL,
  565. AudioRootURL: version.AudioRootURL,
  566. ThemeProfileCode: version.ThemeProfileCode,
  567. PublishedAssetRoot: version.PublishedAssetRoot,
  568. Metadata: decodeJSONMap(version.MetadataJSON),
  569. }
  570. result.Versions = append(result.Versions, view)
  571. if item.CurrentVersionID != nil && *item.CurrentVersionID == version.ID {
  572. result.ResourcePack.CurrentVersion = &AdminResourcePackVersionBrief{
  573. ID: version.PublicID,
  574. VersionCode: version.VersionCode,
  575. Status: version.Status,
  576. }
  577. result.ResourcePack.CurrentVersionID = &view.ID
  578. }
  579. }
  580. return result, nil
  581. }
  582. func (s *AdminResourceService) CreateResourcePackVersion(ctx context.Context, publicID string, input CreateAdminResourcePackVersionInput) (*AdminResourcePackVersion, error) {
  583. item, err := s.store.GetResourcePackByPublicID(ctx, strings.TrimSpace(publicID))
  584. if err != nil {
  585. return nil, err
  586. }
  587. if item == nil {
  588. return nil, apperr.New(http.StatusNotFound, "resource_pack_not_found", "resource pack not found")
  589. }
  590. input.VersionCode = strings.TrimSpace(input.VersionCode)
  591. if input.VersionCode == "" {
  592. return nil, apperr.New(http.StatusBadRequest, "invalid_params", "versionCode is required")
  593. }
  594. publicVersionID, err := security.GeneratePublicID("rpv")
  595. if err != nil {
  596. return nil, err
  597. }
  598. tx, err := s.store.Begin(ctx)
  599. if err != nil {
  600. return nil, err
  601. }
  602. defer tx.Rollback(ctx)
  603. version, err := s.store.CreateResourcePackVersion(ctx, tx, postgres.CreateResourcePackVersionParams{
  604. PublicID: publicVersionID,
  605. ResourcePackID: item.ID,
  606. VersionCode: input.VersionCode,
  607. Status: normalizeVersionStatus(input.Status),
  608. ContentEntryURL: trimStringPtr(input.ContentEntryURL),
  609. AudioRootURL: trimStringPtr(input.AudioRootURL),
  610. ThemeProfileCode: trimStringPtr(input.ThemeProfileCode),
  611. PublishedAssetRoot: trimStringPtr(input.PublishedAssetRoot),
  612. MetadataJSON: input.Metadata,
  613. })
  614. if err != nil {
  615. return nil, err
  616. }
  617. if input.SetAsCurrent {
  618. if err := s.store.SetResourcePackCurrentVersion(ctx, tx, item.ID, version.ID); err != nil {
  619. return nil, err
  620. }
  621. }
  622. if err := tx.Commit(ctx); err != nil {
  623. return nil, err
  624. }
  625. return &AdminResourcePackVersion{
  626. ID: version.PublicID,
  627. VersionCode: version.VersionCode,
  628. Status: version.Status,
  629. ContentEntryURL: version.ContentEntryURL,
  630. AudioRootURL: version.AudioRootURL,
  631. ThemeProfileCode: version.ThemeProfileCode,
  632. PublishedAssetRoot: version.PublishedAssetRoot,
  633. Metadata: decodeJSONMap(version.MetadataJSON),
  634. }, nil
  635. }
  636. func normalizeCatalogStatus(value string) string {
  637. switch strings.TrimSpace(value) {
  638. case "active":
  639. return "active"
  640. case "disabled":
  641. return "disabled"
  642. case "archived":
  643. return "archived"
  644. default:
  645. return "draft"
  646. }
  647. }
  648. func normalizeVersionStatus(value string) string {
  649. switch strings.TrimSpace(value) {
  650. case "active":
  651. return "active"
  652. case "archived":
  653. return "archived"
  654. default:
  655. return "draft"
  656. }
  657. }
  658. func trimStringPtr(value *string) *string {
  659. if value == nil {
  660. return nil
  661. }
  662. trimmed := strings.TrimSpace(*value)
  663. if trimmed == "" {
  664. return nil
  665. }
  666. return &trimmed
  667. }
  668. func decodeJSONMap(raw json.RawMessage) map[string]any {
  669. if len(raw) == 0 {
  670. return nil
  671. }
  672. result := map[string]any{}
  673. if err := json.Unmarshal(raw, &result); err != nil || len(result) == 0 {
  674. return nil
  675. }
  676. return result
  677. }