event_ops_store.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. package postgres
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "github.com/jackc/pgx/v5"
  7. )
  8. type EventPresentation struct {
  9. ID string
  10. PublicID string
  11. EventID string
  12. EventPublicID string
  13. Code string
  14. Name string
  15. PresentationType string
  16. Status string
  17. IsDefault bool
  18. SchemaJSON string
  19. CreatedAt string
  20. UpdatedAt string
  21. }
  22. type ContentBundle struct {
  23. ID string
  24. PublicID string
  25. EventID string
  26. EventPublicID string
  27. Code string
  28. Name string
  29. Status string
  30. IsDefault bool
  31. EntryURL *string
  32. AssetRootURL *string
  33. MetadataJSON string
  34. CreatedAt string
  35. UpdatedAt string
  36. }
  37. type CreateEventPresentationParams struct {
  38. PublicID string
  39. EventID string
  40. Code string
  41. Name string
  42. PresentationType string
  43. Status string
  44. IsDefault bool
  45. SchemaJSON string
  46. }
  47. type CreateContentBundleParams struct {
  48. PublicID string
  49. EventID string
  50. Code string
  51. Name string
  52. Status string
  53. IsDefault bool
  54. EntryURL *string
  55. AssetRootURL *string
  56. MetadataJSON string
  57. }
  58. type EventDefaultBindings struct {
  59. EventID string
  60. EventPublicID string
  61. PresentationID *string
  62. PresentationPublicID *string
  63. PresentationName *string
  64. PresentationType *string
  65. ContentBundleID *string
  66. ContentBundlePublicID *string
  67. ContentBundleName *string
  68. ContentEntryURL *string
  69. ContentAssetRootURL *string
  70. RuntimeBindingID *string
  71. RuntimeBindingPublicID *string
  72. PlacePublicID *string
  73. PlaceName *string
  74. MapAssetPublicID *string
  75. MapAssetName *string
  76. TileReleasePublicID *string
  77. CourseSetPublicID *string
  78. CourseVariantPublicID *string
  79. CourseVariantName *string
  80. RuntimeRouteCode *string
  81. }
  82. type SetEventDefaultBindingsParams struct {
  83. EventID string
  84. PresentationID *string
  85. ContentBundleID *string
  86. RuntimeBindingID *string
  87. UpdatePresentation bool
  88. UpdateContent bool
  89. UpdateRuntime bool
  90. }
  91. func (s *Store) ListEventPresentationsByEventID(ctx context.Context, eventID string, limit int) ([]EventPresentation, error) {
  92. if limit <= 0 || limit > 200 {
  93. limit = 50
  94. }
  95. rows, err := s.pool.Query(ctx, `
  96. SELECT
  97. ep.id,
  98. ep.presentation_public_id,
  99. ep.event_id,
  100. e.event_public_id,
  101. ep.code,
  102. ep.name,
  103. ep.presentation_type,
  104. ep.status,
  105. ep.is_default,
  106. ep.schema_jsonb::text,
  107. ep.created_at::text,
  108. ep.updated_at::text
  109. FROM event_presentations ep
  110. JOIN events e ON e.id = ep.event_id
  111. WHERE ep.event_id = $1
  112. ORDER BY ep.is_default DESC, ep.created_at DESC
  113. LIMIT $2
  114. `, eventID, limit)
  115. if err != nil {
  116. return nil, fmt.Errorf("list event presentations: %w", err)
  117. }
  118. defer rows.Close()
  119. items := []EventPresentation{}
  120. for rows.Next() {
  121. item, err := scanEventPresentationFromRows(rows)
  122. if err != nil {
  123. return nil, err
  124. }
  125. items = append(items, *item)
  126. }
  127. if err := rows.Err(); err != nil {
  128. return nil, fmt.Errorf("iterate event presentations: %w", err)
  129. }
  130. return items, nil
  131. }
  132. func (s *Store) GetEventPresentationByPublicID(ctx context.Context, publicID string) (*EventPresentation, error) {
  133. row := s.pool.QueryRow(ctx, `
  134. SELECT
  135. ep.id,
  136. ep.presentation_public_id,
  137. ep.event_id,
  138. e.event_public_id,
  139. ep.code,
  140. ep.name,
  141. ep.presentation_type,
  142. ep.status,
  143. ep.is_default,
  144. ep.schema_jsonb::text,
  145. ep.created_at::text,
  146. ep.updated_at::text
  147. FROM event_presentations ep
  148. JOIN events e ON e.id = ep.event_id
  149. WHERE ep.presentation_public_id = $1
  150. LIMIT 1
  151. `, publicID)
  152. return scanEventPresentation(row)
  153. }
  154. func (s *Store) GetDefaultEventPresentationByEventID(ctx context.Context, eventID string) (*EventPresentation, error) {
  155. row := s.pool.QueryRow(ctx, `
  156. SELECT
  157. ep.id,
  158. ep.presentation_public_id,
  159. ep.event_id,
  160. e.event_public_id,
  161. ep.code,
  162. ep.name,
  163. ep.presentation_type,
  164. ep.status,
  165. ep.is_default,
  166. ep.schema_jsonb::text,
  167. ep.created_at::text,
  168. ep.updated_at::text
  169. FROM event_presentations ep
  170. JOIN events e ON e.id = ep.event_id
  171. WHERE ep.event_id = $1
  172. AND ep.status = 'active'
  173. ORDER BY ep.is_default DESC, ep.updated_at DESC, ep.created_at DESC
  174. LIMIT 1
  175. `, eventID)
  176. return scanEventPresentation(row)
  177. }
  178. func (s *Store) CreateEventPresentation(ctx context.Context, tx Tx, params CreateEventPresentationParams) (*EventPresentation, error) {
  179. row := tx.QueryRow(ctx, `
  180. INSERT INTO event_presentations (
  181. presentation_public_id,
  182. event_id,
  183. code,
  184. name,
  185. presentation_type,
  186. status,
  187. is_default,
  188. schema_jsonb
  189. )
  190. VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb)
  191. RETURNING
  192. id,
  193. presentation_public_id,
  194. event_id,
  195. code,
  196. name,
  197. presentation_type,
  198. status,
  199. is_default,
  200. schema_jsonb::text,
  201. created_at::text,
  202. updated_at::text
  203. `, params.PublicID, params.EventID, params.Code, params.Name, params.PresentationType, params.Status, params.IsDefault, params.SchemaJSON)
  204. var item EventPresentation
  205. if err := row.Scan(
  206. &item.ID,
  207. &item.PublicID,
  208. &item.EventID,
  209. &item.Code,
  210. &item.Name,
  211. &item.PresentationType,
  212. &item.Status,
  213. &item.IsDefault,
  214. &item.SchemaJSON,
  215. &item.CreatedAt,
  216. &item.UpdatedAt,
  217. ); err != nil {
  218. return nil, fmt.Errorf("create event presentation: %w", err)
  219. }
  220. return &item, nil
  221. }
  222. func (s *Store) GetEventDefaultBindingsByEventID(ctx context.Context, eventID string) (*EventDefaultBindings, error) {
  223. row := s.pool.QueryRow(ctx, `
  224. SELECT
  225. e.id,
  226. e.event_public_id,
  227. e.current_presentation_id,
  228. ep.presentation_public_id,
  229. ep.name,
  230. ep.presentation_type,
  231. e.current_content_bundle_id,
  232. cb.content_bundle_public_id,
  233. cb.name,
  234. cb.entry_url,
  235. cb.asset_root_url,
  236. e.current_runtime_binding_id,
  237. mrb.runtime_binding_public_id,
  238. p.place_public_id,
  239. p.name,
  240. ma.map_asset_public_id,
  241. ma.name,
  242. tr.tile_release_public_id,
  243. cset.course_set_public_id,
  244. cv.course_variant_public_id,
  245. cv.name,
  246. cv.route_code
  247. FROM events e
  248. LEFT JOIN event_presentations ep ON ep.id = e.current_presentation_id
  249. LEFT JOIN content_bundles cb ON cb.id = e.current_content_bundle_id
  250. LEFT JOIN map_runtime_bindings mrb ON mrb.id = e.current_runtime_binding_id
  251. LEFT JOIN places p ON p.id = mrb.place_id
  252. LEFT JOIN map_assets ma ON ma.id = mrb.map_asset_id
  253. LEFT JOIN tile_releases tr ON tr.id = mrb.tile_release_id
  254. LEFT JOIN course_sets cset ON cset.id = mrb.course_set_id
  255. LEFT JOIN course_variants cv ON cv.id = mrb.course_variant_id
  256. WHERE e.id = $1
  257. LIMIT 1
  258. `, eventID)
  259. return scanEventDefaultBindings(row)
  260. }
  261. func (s *Store) SetEventDefaultBindings(ctx context.Context, tx Tx, params SetEventDefaultBindingsParams) error {
  262. if _, err := tx.Exec(ctx, `
  263. UPDATE events
  264. SET current_presentation_id = CASE WHEN $5 THEN $2 ELSE current_presentation_id END,
  265. current_content_bundle_id = CASE WHEN $6 THEN $3 ELSE current_content_bundle_id END,
  266. current_runtime_binding_id = CASE WHEN $7 THEN $4 ELSE current_runtime_binding_id END
  267. WHERE id = $1
  268. `, params.EventID, params.PresentationID, params.ContentBundleID, params.RuntimeBindingID, params.UpdatePresentation, params.UpdateContent, params.UpdateRuntime); err != nil {
  269. return fmt.Errorf("set event default bindings: %w", err)
  270. }
  271. return nil
  272. }
  273. func (s *Store) ListContentBundlesByEventID(ctx context.Context, eventID string, limit int) ([]ContentBundle, error) {
  274. if limit <= 0 || limit > 200 {
  275. limit = 50
  276. }
  277. rows, err := s.pool.Query(ctx, `
  278. SELECT
  279. cb.id,
  280. cb.content_bundle_public_id,
  281. cb.event_id,
  282. e.event_public_id,
  283. cb.code,
  284. cb.name,
  285. cb.status,
  286. cb.is_default,
  287. cb.entry_url,
  288. cb.asset_root_url,
  289. cb.metadata_jsonb::text,
  290. cb.created_at::text,
  291. cb.updated_at::text
  292. FROM content_bundles cb
  293. JOIN events e ON e.id = cb.event_id
  294. WHERE cb.event_id = $1
  295. ORDER BY cb.is_default DESC, cb.created_at DESC
  296. LIMIT $2
  297. `, eventID, limit)
  298. if err != nil {
  299. return nil, fmt.Errorf("list content bundles: %w", err)
  300. }
  301. defer rows.Close()
  302. items := []ContentBundle{}
  303. for rows.Next() {
  304. item, err := scanContentBundleFromRows(rows)
  305. if err != nil {
  306. return nil, err
  307. }
  308. items = append(items, *item)
  309. }
  310. if err := rows.Err(); err != nil {
  311. return nil, fmt.Errorf("iterate content bundles: %w", err)
  312. }
  313. return items, nil
  314. }
  315. func (s *Store) GetContentBundleByPublicID(ctx context.Context, publicID string) (*ContentBundle, error) {
  316. row := s.pool.QueryRow(ctx, `
  317. SELECT
  318. cb.id,
  319. cb.content_bundle_public_id,
  320. cb.event_id,
  321. e.event_public_id,
  322. cb.code,
  323. cb.name,
  324. cb.status,
  325. cb.is_default,
  326. cb.entry_url,
  327. cb.asset_root_url,
  328. cb.metadata_jsonb::text,
  329. cb.created_at::text,
  330. cb.updated_at::text
  331. FROM content_bundles cb
  332. JOIN events e ON e.id = cb.event_id
  333. WHERE cb.content_bundle_public_id = $1
  334. LIMIT 1
  335. `, publicID)
  336. return scanContentBundle(row)
  337. }
  338. func (s *Store) GetDefaultContentBundleByEventID(ctx context.Context, eventID string) (*ContentBundle, error) {
  339. row := s.pool.QueryRow(ctx, `
  340. SELECT
  341. cb.id,
  342. cb.content_bundle_public_id,
  343. cb.event_id,
  344. e.event_public_id,
  345. cb.code,
  346. cb.name,
  347. cb.status,
  348. cb.is_default,
  349. cb.entry_url,
  350. cb.asset_root_url,
  351. cb.metadata_jsonb::text,
  352. cb.created_at::text,
  353. cb.updated_at::text
  354. FROM content_bundles cb
  355. JOIN events e ON e.id = cb.event_id
  356. WHERE cb.event_id = $1
  357. AND cb.status = 'active'
  358. ORDER BY cb.is_default DESC, cb.updated_at DESC, cb.created_at DESC
  359. LIMIT 1
  360. `, eventID)
  361. return scanContentBundle(row)
  362. }
  363. func (s *Store) CreateContentBundle(ctx context.Context, tx Tx, params CreateContentBundleParams) (*ContentBundle, error) {
  364. row := tx.QueryRow(ctx, `
  365. INSERT INTO content_bundles (
  366. content_bundle_public_id,
  367. event_id,
  368. code,
  369. name,
  370. status,
  371. is_default,
  372. entry_url,
  373. asset_root_url,
  374. metadata_jsonb
  375. )
  376. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)
  377. RETURNING
  378. id,
  379. content_bundle_public_id,
  380. event_id,
  381. code,
  382. name,
  383. status,
  384. is_default,
  385. entry_url,
  386. asset_root_url,
  387. metadata_jsonb::text,
  388. created_at::text,
  389. updated_at::text
  390. `, params.PublicID, params.EventID, params.Code, params.Name, params.Status, params.IsDefault, params.EntryURL, params.AssetRootURL, params.MetadataJSON)
  391. var item ContentBundle
  392. if err := row.Scan(
  393. &item.ID,
  394. &item.PublicID,
  395. &item.EventID,
  396. &item.Code,
  397. &item.Name,
  398. &item.Status,
  399. &item.IsDefault,
  400. &item.EntryURL,
  401. &item.AssetRootURL,
  402. &item.MetadataJSON,
  403. &item.CreatedAt,
  404. &item.UpdatedAt,
  405. ); err != nil {
  406. return nil, fmt.Errorf("create content bundle: %w", err)
  407. }
  408. return &item, nil
  409. }
  410. func scanEventPresentation(row pgx.Row) (*EventPresentation, error) {
  411. var item EventPresentation
  412. err := row.Scan(
  413. &item.ID,
  414. &item.PublicID,
  415. &item.EventID,
  416. &item.EventPublicID,
  417. &item.Code,
  418. &item.Name,
  419. &item.PresentationType,
  420. &item.Status,
  421. &item.IsDefault,
  422. &item.SchemaJSON,
  423. &item.CreatedAt,
  424. &item.UpdatedAt,
  425. )
  426. if errors.Is(err, pgx.ErrNoRows) {
  427. return nil, nil
  428. }
  429. if err != nil {
  430. return nil, fmt.Errorf("scan event presentation: %w", err)
  431. }
  432. return &item, nil
  433. }
  434. func scanEventPresentationFromRows(rows pgx.Rows) (*EventPresentation, error) {
  435. var item EventPresentation
  436. if err := rows.Scan(
  437. &item.ID,
  438. &item.PublicID,
  439. &item.EventID,
  440. &item.EventPublicID,
  441. &item.Code,
  442. &item.Name,
  443. &item.PresentationType,
  444. &item.Status,
  445. &item.IsDefault,
  446. &item.SchemaJSON,
  447. &item.CreatedAt,
  448. &item.UpdatedAt,
  449. ); err != nil {
  450. return nil, fmt.Errorf("scan event presentation row: %w", err)
  451. }
  452. return &item, nil
  453. }
  454. func scanContentBundle(row pgx.Row) (*ContentBundle, error) {
  455. var item ContentBundle
  456. err := row.Scan(
  457. &item.ID,
  458. &item.PublicID,
  459. &item.EventID,
  460. &item.EventPublicID,
  461. &item.Code,
  462. &item.Name,
  463. &item.Status,
  464. &item.IsDefault,
  465. &item.EntryURL,
  466. &item.AssetRootURL,
  467. &item.MetadataJSON,
  468. &item.CreatedAt,
  469. &item.UpdatedAt,
  470. )
  471. if errors.Is(err, pgx.ErrNoRows) {
  472. return nil, nil
  473. }
  474. if err != nil {
  475. return nil, fmt.Errorf("scan content bundle: %w", err)
  476. }
  477. return &item, nil
  478. }
  479. func scanContentBundleFromRows(rows pgx.Rows) (*ContentBundle, error) {
  480. var item ContentBundle
  481. if err := rows.Scan(
  482. &item.ID,
  483. &item.PublicID,
  484. &item.EventID,
  485. &item.EventPublicID,
  486. &item.Code,
  487. &item.Name,
  488. &item.Status,
  489. &item.IsDefault,
  490. &item.EntryURL,
  491. &item.AssetRootURL,
  492. &item.MetadataJSON,
  493. &item.CreatedAt,
  494. &item.UpdatedAt,
  495. ); err != nil {
  496. return nil, fmt.Errorf("scan content bundle row: %w", err)
  497. }
  498. return &item, nil
  499. }
  500. func scanEventDefaultBindings(row pgx.Row) (*EventDefaultBindings, error) {
  501. var item EventDefaultBindings
  502. err := row.Scan(
  503. &item.EventID,
  504. &item.EventPublicID,
  505. &item.PresentationID,
  506. &item.PresentationPublicID,
  507. &item.PresentationName,
  508. &item.PresentationType,
  509. &item.ContentBundleID,
  510. &item.ContentBundlePublicID,
  511. &item.ContentBundleName,
  512. &item.ContentEntryURL,
  513. &item.ContentAssetRootURL,
  514. &item.RuntimeBindingID,
  515. &item.RuntimeBindingPublicID,
  516. &item.PlacePublicID,
  517. &item.PlaceName,
  518. &item.MapAssetPublicID,
  519. &item.MapAssetName,
  520. &item.TileReleasePublicID,
  521. &item.CourseSetPublicID,
  522. &item.CourseVariantPublicID,
  523. &item.CourseVariantName,
  524. &item.RuntimeRouteCode,
  525. )
  526. if errors.Is(err, pgx.ErrNoRows) {
  527. return nil, nil
  528. }
  529. if err != nil {
  530. return nil, fmt.Errorf("scan event default bindings: %w", err)
  531. }
  532. return &item, nil
  533. }