production_store.go 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. package postgres
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "time"
  8. "github.com/jackc/pgx/v5"
  9. )
  10. type Place struct {
  11. ID string
  12. PublicID string
  13. Code string
  14. Name string
  15. Region *string
  16. CoverURL *string
  17. Description *string
  18. CenterPoint json.RawMessage
  19. Status string
  20. CreatedAt time.Time
  21. UpdatedAt time.Time
  22. }
  23. type MapAsset struct {
  24. ID string
  25. PublicID string
  26. PlaceID string
  27. LegacyMapID *string
  28. LegacyMapPublicID *string
  29. Code string
  30. Name string
  31. MapType string
  32. CoverURL *string
  33. Description *string
  34. Status string
  35. CurrentTileReleaseID *string
  36. CreatedAt time.Time
  37. UpdatedAt time.Time
  38. }
  39. type TileRelease struct {
  40. ID string
  41. PublicID string
  42. MapAssetID string
  43. LegacyMapVersionID *string
  44. LegacyMapVersionPub *string
  45. VersionCode string
  46. Status string
  47. TileBaseURL string
  48. MetaURL string
  49. PublishedAssetRoot *string
  50. MetadataJSON json.RawMessage
  51. PublishedAt *time.Time
  52. CreatedAt time.Time
  53. UpdatedAt time.Time
  54. }
  55. type CourseSource struct {
  56. ID string
  57. PublicID string
  58. LegacyPlayfieldVersionID *string
  59. LegacyPlayfieldVersionPub *string
  60. SourceType string
  61. FileURL string
  62. Checksum *string
  63. ParserVersion *string
  64. ImportStatus string
  65. MetadataJSON json.RawMessage
  66. ImportedAt time.Time
  67. CreatedAt time.Time
  68. UpdatedAt time.Time
  69. }
  70. type CourseSet struct {
  71. ID string
  72. PublicID string
  73. PlaceID string
  74. MapAssetID string
  75. Code string
  76. Mode string
  77. Name string
  78. Description *string
  79. Status string
  80. CurrentVariantID *string
  81. CreatedAt time.Time
  82. UpdatedAt time.Time
  83. }
  84. type CourseVariant struct {
  85. ID string
  86. PublicID string
  87. CourseSetID string
  88. SourceID *string
  89. SourcePublicID *string
  90. Name string
  91. RouteCode *string
  92. Mode string
  93. ControlCount *int
  94. Difficulty *string
  95. Status string
  96. IsDefault bool
  97. ConfigPatch json.RawMessage
  98. MetadataJSON json.RawMessage
  99. CreatedAt time.Time
  100. UpdatedAt time.Time
  101. }
  102. type MapRuntimeBinding struct {
  103. ID string
  104. PublicID string
  105. EventID string
  106. EventPublicID string
  107. PlaceID string
  108. PlacePublicID string
  109. MapAssetID string
  110. MapAssetPublicID string
  111. TileReleaseID string
  112. TileReleasePublicID string
  113. CourseSetID string
  114. CourseSetPublicID string
  115. CourseVariantID string
  116. CourseVariantPublicID string
  117. Status string
  118. Notes *string
  119. CreatedAt time.Time
  120. UpdatedAt time.Time
  121. }
  122. type CreatePlaceParams struct {
  123. PublicID string
  124. Code string
  125. Name string
  126. Region *string
  127. CoverURL *string
  128. Description *string
  129. CenterPoint map[string]any
  130. Status string
  131. }
  132. type CreateMapAssetParams struct {
  133. PublicID string
  134. PlaceID string
  135. LegacyMapID *string
  136. Code string
  137. Name string
  138. MapType string
  139. CoverURL *string
  140. Description *string
  141. Status string
  142. }
  143. type CreateTileReleaseParams struct {
  144. PublicID string
  145. MapAssetID string
  146. LegacyMapVersionID *string
  147. VersionCode string
  148. Status string
  149. TileBaseURL string
  150. MetaURL string
  151. PublishedAssetRoot *string
  152. MetadataJSON map[string]any
  153. PublishedAt *time.Time
  154. }
  155. type CreateCourseSourceParams struct {
  156. PublicID string
  157. LegacyPlayfieldVersionID *string
  158. SourceType string
  159. FileURL string
  160. Checksum *string
  161. ParserVersion *string
  162. ImportStatus string
  163. MetadataJSON map[string]any
  164. ImportedAt *time.Time
  165. }
  166. type CreateCourseSetParams struct {
  167. PublicID string
  168. PlaceID string
  169. MapAssetID string
  170. Code string
  171. Mode string
  172. Name string
  173. Description *string
  174. Status string
  175. }
  176. type CreateCourseVariantParams struct {
  177. PublicID string
  178. CourseSetID string
  179. SourceID *string
  180. Name string
  181. RouteCode *string
  182. Mode string
  183. ControlCount *int
  184. Difficulty *string
  185. Status string
  186. IsDefault bool
  187. ConfigPatch map[string]any
  188. MetadataJSON map[string]any
  189. }
  190. type CreateMapRuntimeBindingParams struct {
  191. PublicID string
  192. EventID string
  193. PlaceID string
  194. MapAssetID string
  195. TileReleaseID string
  196. CourseSetID string
  197. CourseVariantID string
  198. Status string
  199. Notes *string
  200. }
  201. func (s *Store) ListPlaces(ctx context.Context, limit int) ([]Place, error) {
  202. if limit <= 0 || limit > 200 {
  203. limit = 50
  204. }
  205. rows, err := s.pool.Query(ctx, `
  206. SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
  207. FROM places
  208. ORDER BY created_at DESC
  209. LIMIT $1
  210. `, limit)
  211. if err != nil {
  212. return nil, fmt.Errorf("list places: %w", err)
  213. }
  214. defer rows.Close()
  215. items := []Place{}
  216. for rows.Next() {
  217. item, err := scanPlaceFromRows(rows)
  218. if err != nil {
  219. return nil, err
  220. }
  221. items = append(items, *item)
  222. }
  223. if err := rows.Err(); err != nil {
  224. return nil, fmt.Errorf("iterate places: %w", err)
  225. }
  226. return items, nil
  227. }
  228. func (s *Store) GetPlaceByPublicID(ctx context.Context, publicID string) (*Place, error) {
  229. row := s.pool.QueryRow(ctx, `
  230. SELECT id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
  231. FROM places
  232. WHERE place_public_id = $1
  233. LIMIT 1
  234. `, publicID)
  235. return scanPlace(row)
  236. }
  237. func (s *Store) CreatePlace(ctx context.Context, tx Tx, params CreatePlaceParams) (*Place, error) {
  238. centerPointJSON, err := marshalJSONMap(params.CenterPoint)
  239. if err != nil {
  240. return nil, fmt.Errorf("marshal place center point: %w", err)
  241. }
  242. row := tx.QueryRow(ctx, `
  243. INSERT INTO places (place_public_id, code, name, region, cover_url, description, center_point_jsonb, status)
  244. VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8)
  245. RETURNING id, place_public_id, code, name, region, cover_url, description, center_point_jsonb::text, status, created_at, updated_at
  246. `, params.PublicID, params.Code, params.Name, params.Region, params.CoverURL, params.Description, centerPointJSON, params.Status)
  247. return scanPlace(row)
  248. }
  249. func (s *Store) ListMapAssetsByPlaceID(ctx context.Context, placeID string) ([]MapAsset, error) {
  250. rows, err := s.pool.Query(ctx, `
  251. SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
  252. ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
  253. FROM map_assets ma
  254. LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
  255. WHERE ma.place_id = $1
  256. ORDER BY ma.created_at DESC
  257. `, placeID)
  258. if err != nil {
  259. return nil, fmt.Errorf("list map assets: %w", err)
  260. }
  261. defer rows.Close()
  262. items := []MapAsset{}
  263. for rows.Next() {
  264. item, err := scanMapAssetFromRows(rows)
  265. if err != nil {
  266. return nil, err
  267. }
  268. items = append(items, *item)
  269. }
  270. if err := rows.Err(); err != nil {
  271. return nil, fmt.Errorf("iterate map assets: %w", err)
  272. }
  273. return items, nil
  274. }
  275. func (s *Store) GetMapAssetByPublicID(ctx context.Context, publicID string) (*MapAsset, error) {
  276. row := s.pool.QueryRow(ctx, `
  277. SELECT ma.id, ma.map_asset_public_id, ma.place_id, ma.legacy_map_id, lm.map_public_id, ma.code, ma.name, ma.map_type,
  278. ma.cover_url, ma.description, ma.status, ma.current_tile_release_id, ma.created_at, ma.updated_at
  279. FROM map_assets ma
  280. LEFT JOIN maps lm ON lm.id = ma.legacy_map_id
  281. WHERE ma.map_asset_public_id = $1
  282. LIMIT 1
  283. `, publicID)
  284. return scanMapAsset(row)
  285. }
  286. func (s *Store) CreateMapAsset(ctx context.Context, tx Tx, params CreateMapAssetParams) (*MapAsset, error) {
  287. row := tx.QueryRow(ctx, `
  288. INSERT INTO map_assets (map_asset_public_id, place_id, legacy_map_id, code, name, map_type, cover_url, description, status)
  289. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
  290. RETURNING id, map_asset_public_id, place_id, legacy_map_id, NULL::text, code, name, map_type, cover_url, description, status, current_tile_release_id, created_at, updated_at
  291. `, params.PublicID, params.PlaceID, params.LegacyMapID, params.Code, params.Name, params.MapType, params.CoverURL, params.Description, params.Status)
  292. return scanMapAsset(row)
  293. }
  294. func (s *Store) ListTileReleasesByMapAssetID(ctx context.Context, mapAssetID string) ([]TileRelease, error) {
  295. rows, err := s.pool.Query(ctx, `
  296. SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
  297. tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
  298. tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
  299. FROM tile_releases tr
  300. LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
  301. WHERE tr.map_asset_id = $1
  302. ORDER BY tr.created_at DESC
  303. `, mapAssetID)
  304. if err != nil {
  305. return nil, fmt.Errorf("list tile releases: %w", err)
  306. }
  307. defer rows.Close()
  308. items := []TileRelease{}
  309. for rows.Next() {
  310. item, err := scanTileReleaseFromRows(rows)
  311. if err != nil {
  312. return nil, err
  313. }
  314. items = append(items, *item)
  315. }
  316. if err := rows.Err(); err != nil {
  317. return nil, fmt.Errorf("iterate tile releases: %w", err)
  318. }
  319. return items, nil
  320. }
  321. func (s *Store) GetTileReleaseByPublicID(ctx context.Context, publicID string) (*TileRelease, error) {
  322. row := s.pool.QueryRow(ctx, `
  323. SELECT tr.id, tr.tile_release_public_id, tr.map_asset_id, tr.legacy_map_version_id, mv.version_public_id,
  324. tr.version_code, tr.status, tr.tile_base_url, tr.meta_url, tr.published_asset_root,
  325. tr.metadata_jsonb::text, tr.published_at, tr.created_at, tr.updated_at
  326. FROM tile_releases tr
  327. LEFT JOIN map_versions mv ON mv.id = tr.legacy_map_version_id
  328. WHERE tr.tile_release_public_id = $1
  329. LIMIT 1
  330. `, publicID)
  331. return scanTileRelease(row)
  332. }
  333. func (s *Store) CreateTileRelease(ctx context.Context, tx Tx, params CreateTileReleaseParams) (*TileRelease, error) {
  334. metadataJSON, err := marshalJSONMap(params.MetadataJSON)
  335. if err != nil {
  336. return nil, fmt.Errorf("marshal tile release metadata: %w", err)
  337. }
  338. row := tx.QueryRow(ctx, `
  339. INSERT INTO tile_releases (
  340. tile_release_public_id, map_asset_id, legacy_map_version_id, version_code, status,
  341. tile_base_url, meta_url, published_asset_root, metadata_jsonb, published_at
  342. )
  343. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb, $10)
  344. RETURNING id, tile_release_public_id, map_asset_id, legacy_map_version_id, NULL::text, version_code, status,
  345. tile_base_url, meta_url, published_asset_root, metadata_jsonb::text, published_at, created_at, updated_at
  346. `, params.PublicID, params.MapAssetID, params.LegacyMapVersionID, params.VersionCode, params.Status, params.TileBaseURL, params.MetaURL, params.PublishedAssetRoot, metadataJSON, params.PublishedAt)
  347. return scanTileRelease(row)
  348. }
  349. func (s *Store) SetMapAssetCurrentTileRelease(ctx context.Context, tx Tx, mapAssetID, tileReleaseID string) error {
  350. _, err := tx.Exec(ctx, `UPDATE map_assets SET current_tile_release_id = $2 WHERE id = $1`, mapAssetID, tileReleaseID)
  351. if err != nil {
  352. return fmt.Errorf("set map asset current tile release: %w", err)
  353. }
  354. return nil
  355. }
  356. func (s *Store) ListCourseSources(ctx context.Context, limit int) ([]CourseSource, error) {
  357. if limit <= 0 || limit > 200 {
  358. limit = 50
  359. }
  360. rows, err := s.pool.Query(ctx, `
  361. SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
  362. cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
  363. FROM course_sources cs
  364. LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
  365. ORDER BY cs.created_at DESC
  366. LIMIT $1
  367. `, limit)
  368. if err != nil {
  369. return nil, fmt.Errorf("list course sources: %w", err)
  370. }
  371. defer rows.Close()
  372. items := []CourseSource{}
  373. for rows.Next() {
  374. item, err := scanCourseSourceFromRows(rows)
  375. if err != nil {
  376. return nil, err
  377. }
  378. items = append(items, *item)
  379. }
  380. if err := rows.Err(); err != nil {
  381. return nil, fmt.Errorf("iterate course sources: %w", err)
  382. }
  383. return items, nil
  384. }
  385. func (s *Store) GetCourseSourceByPublicID(ctx context.Context, publicID string) (*CourseSource, error) {
  386. row := s.pool.QueryRow(ctx, `
  387. SELECT cs.id, cs.course_source_public_id, cs.legacy_playfield_version_id, pv.version_public_id, cs.source_type,
  388. cs.file_url, cs.checksum, cs.parser_version, cs.import_status, cs.metadata_jsonb::text, cs.imported_at, cs.created_at, cs.updated_at
  389. FROM course_sources cs
  390. LEFT JOIN playfield_versions pv ON pv.id = cs.legacy_playfield_version_id
  391. WHERE cs.course_source_public_id = $1
  392. LIMIT 1
  393. `, publicID)
  394. return scanCourseSource(row)
  395. }
  396. func (s *Store) CreateCourseSource(ctx context.Context, tx Tx, params CreateCourseSourceParams) (*CourseSource, error) {
  397. metadataJSON, err := marshalJSONMap(params.MetadataJSON)
  398. if err != nil {
  399. return nil, fmt.Errorf("marshal course source metadata: %w", err)
  400. }
  401. importedAt := time.Now()
  402. if params.ImportedAt != nil {
  403. importedAt = *params.ImportedAt
  404. }
  405. row := tx.QueryRow(ctx, `
  406. INSERT INTO course_sources (
  407. course_source_public_id, legacy_playfield_version_id, source_type, file_url, checksum,
  408. parser_version, import_status, metadata_jsonb, imported_at
  409. )
  410. VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
  411. RETURNING id, course_source_public_id, legacy_playfield_version_id, NULL::text, source_type, file_url,
  412. checksum, parser_version, import_status, metadata_jsonb::text, imported_at, created_at, updated_at
  413. `, params.PublicID, params.LegacyPlayfieldVersionID, params.SourceType, params.FileURL, params.Checksum, params.ParserVersion, params.ImportStatus, metadataJSON, importedAt)
  414. return scanCourseSource(row)
  415. }
  416. func (s *Store) ListCourseSets(ctx context.Context, limit int) ([]CourseSet, error) {
  417. if limit <= 0 || limit > 200 {
  418. limit = 50
  419. }
  420. rows, err := s.pool.Query(ctx, `
  421. SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
  422. FROM course_sets
  423. ORDER BY created_at DESC
  424. LIMIT $1
  425. `, limit)
  426. if err != nil {
  427. return nil, fmt.Errorf("list course sets: %w", err)
  428. }
  429. defer rows.Close()
  430. items := []CourseSet{}
  431. for rows.Next() {
  432. item, err := scanCourseSetFromRows(rows)
  433. if err != nil {
  434. return nil, err
  435. }
  436. items = append(items, *item)
  437. }
  438. if err := rows.Err(); err != nil {
  439. return nil, fmt.Errorf("iterate course sets: %w", err)
  440. }
  441. return items, nil
  442. }
  443. func (s *Store) ListCourseSetsByMapAssetID(ctx context.Context, mapAssetID string) ([]CourseSet, error) {
  444. rows, err := s.pool.Query(ctx, `
  445. SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
  446. FROM course_sets
  447. WHERE map_asset_id = $1
  448. ORDER BY created_at DESC
  449. `, mapAssetID)
  450. if err != nil {
  451. return nil, fmt.Errorf("list course sets by map asset: %w", err)
  452. }
  453. defer rows.Close()
  454. items := []CourseSet{}
  455. for rows.Next() {
  456. item, err := scanCourseSetFromRows(rows)
  457. if err != nil {
  458. return nil, err
  459. }
  460. items = append(items, *item)
  461. }
  462. if err := rows.Err(); err != nil {
  463. return nil, fmt.Errorf("iterate course sets by map asset: %w", err)
  464. }
  465. return items, nil
  466. }
  467. func (s *Store) GetCourseSetByPublicID(ctx context.Context, publicID string) (*CourseSet, error) {
  468. row := s.pool.QueryRow(ctx, `
  469. SELECT id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
  470. FROM course_sets
  471. WHERE course_set_public_id = $1
  472. LIMIT 1
  473. `, publicID)
  474. return scanCourseSet(row)
  475. }
  476. func (s *Store) CreateCourseSet(ctx context.Context, tx Tx, params CreateCourseSetParams) (*CourseSet, error) {
  477. row := tx.QueryRow(ctx, `
  478. INSERT INTO course_sets (course_set_public_id, place_id, map_asset_id, code, mode, name, description, status)
  479. VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
  480. RETURNING id, course_set_public_id, place_id, map_asset_id, code, mode, name, description, status, current_variant_id, created_at, updated_at
  481. `, params.PublicID, params.PlaceID, params.MapAssetID, params.Code, params.Mode, params.Name, params.Description, params.Status)
  482. return scanCourseSet(row)
  483. }
  484. func (s *Store) ListCourseVariantsByCourseSetID(ctx context.Context, courseSetID string) ([]CourseVariant, error) {
  485. rows, err := s.pool.Query(ctx, `
  486. SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
  487. cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
  488. cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
  489. FROM course_variants cv
  490. LEFT JOIN course_sources cs ON cs.id = cv.source_id
  491. WHERE cv.course_set_id = $1
  492. ORDER BY cv.created_at DESC
  493. `, courseSetID)
  494. if err != nil {
  495. return nil, fmt.Errorf("list course variants: %w", err)
  496. }
  497. defer rows.Close()
  498. items := []CourseVariant{}
  499. for rows.Next() {
  500. item, err := scanCourseVariantFromRows(rows)
  501. if err != nil {
  502. return nil, err
  503. }
  504. items = append(items, *item)
  505. }
  506. if err := rows.Err(); err != nil {
  507. return nil, fmt.Errorf("iterate course variants: %w", err)
  508. }
  509. return items, nil
  510. }
  511. func (s *Store) GetCourseVariantByPublicID(ctx context.Context, publicID string) (*CourseVariant, error) {
  512. row := s.pool.QueryRow(ctx, `
  513. SELECT cv.id, cv.course_variant_public_id, cv.course_set_id, cv.source_id, cs.course_source_public_id, cv.name, cv.route_code,
  514. cv.mode, cv.control_count, cv.difficulty, cv.status, cv.is_default,
  515. cv.config_patch_jsonb::text, cv.metadata_jsonb::text, cv.created_at, cv.updated_at
  516. FROM course_variants cv
  517. LEFT JOIN course_sources cs ON cs.id = cv.source_id
  518. WHERE cv.course_variant_public_id = $1
  519. LIMIT 1
  520. `, publicID)
  521. return scanCourseVariant(row)
  522. }
  523. func (s *Store) CreateCourseVariant(ctx context.Context, tx Tx, params CreateCourseVariantParams) (*CourseVariant, error) {
  524. configPatchJSON, err := marshalJSONMap(params.ConfigPatch)
  525. if err != nil {
  526. return nil, fmt.Errorf("marshal course variant config patch: %w", err)
  527. }
  528. metadataJSON, err := marshalJSONMap(params.MetadataJSON)
  529. if err != nil {
  530. return nil, fmt.Errorf("marshal course variant metadata: %w", err)
  531. }
  532. row := tx.QueryRow(ctx, `
  533. INSERT INTO course_variants (
  534. course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count,
  535. difficulty, status, is_default, config_patch_jsonb, metadata_jsonb
  536. )
  537. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11::jsonb, $12::jsonb)
  538. RETURNING id, course_variant_public_id, course_set_id, source_id, NULL::text, name, route_code, mode,
  539. control_count, difficulty, status, is_default, config_patch_jsonb::text, metadata_jsonb::text, created_at, updated_at
  540. `, params.PublicID, params.CourseSetID, params.SourceID, params.Name, params.RouteCode, params.Mode, params.ControlCount, params.Difficulty, params.Status, params.IsDefault, configPatchJSON, metadataJSON)
  541. return scanCourseVariant(row)
  542. }
  543. func (s *Store) SetCourseSetCurrentVariant(ctx context.Context, tx Tx, courseSetID, variantID string) error {
  544. _, err := tx.Exec(ctx, `UPDATE course_sets SET current_variant_id = $2 WHERE id = $1`, courseSetID, variantID)
  545. if err != nil {
  546. return fmt.Errorf("set course set current variant: %w", err)
  547. }
  548. return nil
  549. }
  550. func (s *Store) ListMapRuntimeBindings(ctx context.Context, limit int) ([]MapRuntimeBinding, error) {
  551. if limit <= 0 || limit > 200 {
  552. limit = 50
  553. }
  554. rows, err := s.pool.Query(ctx, `
  555. SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
  556. mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
  557. mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
  558. mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
  559. FROM map_runtime_bindings mrb
  560. JOIN events e ON e.id = mrb.event_id
  561. JOIN places p ON p.id = mrb.place_id
  562. JOIN map_assets ma ON ma.id = mrb.map_asset_id
  563. JOIN tile_releases tr ON tr.id = mrb.tile_release_id
  564. JOIN course_sets cset ON cset.id = mrb.course_set_id
  565. JOIN course_variants cv ON cv.id = mrb.course_variant_id
  566. ORDER BY mrb.created_at DESC
  567. LIMIT $1
  568. `, limit)
  569. if err != nil {
  570. return nil, fmt.Errorf("list runtime bindings: %w", err)
  571. }
  572. defer rows.Close()
  573. items := []MapRuntimeBinding{}
  574. for rows.Next() {
  575. item, err := scanMapRuntimeBindingFromRows(rows)
  576. if err != nil {
  577. return nil, err
  578. }
  579. items = append(items, *item)
  580. }
  581. if err := rows.Err(); err != nil {
  582. return nil, fmt.Errorf("iterate runtime bindings: %w", err)
  583. }
  584. return items, nil
  585. }
  586. func (s *Store) GetMapRuntimeBindingByPublicID(ctx context.Context, publicID string) (*MapRuntimeBinding, error) {
  587. row := s.pool.QueryRow(ctx, `
  588. SELECT mrb.id, mrb.runtime_binding_public_id, mrb.event_id, e.event_public_id, mrb.place_id, p.place_public_id,
  589. mrb.map_asset_id, ma.map_asset_public_id, mrb.tile_release_id, tr.tile_release_public_id,
  590. mrb.course_set_id, cset.course_set_public_id, mrb.course_variant_id, cv.course_variant_public_id,
  591. mrb.status, mrb.notes, mrb.created_at, mrb.updated_at
  592. FROM map_runtime_bindings mrb
  593. JOIN events e ON e.id = mrb.event_id
  594. JOIN places p ON p.id = mrb.place_id
  595. JOIN map_assets ma ON ma.id = mrb.map_asset_id
  596. JOIN tile_releases tr ON tr.id = mrb.tile_release_id
  597. JOIN course_sets cset ON cset.id = mrb.course_set_id
  598. JOIN course_variants cv ON cv.id = mrb.course_variant_id
  599. WHERE mrb.runtime_binding_public_id = $1
  600. LIMIT 1
  601. `, publicID)
  602. return scanMapRuntimeBinding(row)
  603. }
  604. func (s *Store) CreateMapRuntimeBinding(ctx context.Context, tx Tx, params CreateMapRuntimeBindingParams) (*MapRuntimeBinding, error) {
  605. row := tx.QueryRow(ctx, `
  606. INSERT INTO map_runtime_bindings (
  607. runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
  608. )
  609. VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
  610. RETURNING id, runtime_binding_public_id, event_id, ''::text, place_id, ''::text, map_asset_id, ''::text,
  611. tile_release_id, ''::text, course_set_id, ''::text, course_variant_id, ''::text,
  612. status, notes, created_at, updated_at
  613. `, params.PublicID, params.EventID, params.PlaceID, params.MapAssetID, params.TileReleaseID, params.CourseSetID, params.CourseVariantID, params.Status, params.Notes)
  614. return scanMapRuntimeBinding(row)
  615. }
  616. func scanPlace(row pgx.Row) (*Place, error) {
  617. var item Place
  618. var centerPoint string
  619. err := row.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, &centerPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
  620. if errors.Is(err, pgx.ErrNoRows) {
  621. return nil, nil
  622. }
  623. if err != nil {
  624. return nil, fmt.Errorf("scan place: %w", err)
  625. }
  626. item.CenterPoint = json.RawMessage(centerPoint)
  627. return &item, nil
  628. }
  629. func scanPlaceFromRows(rows pgx.Rows) (*Place, error) {
  630. var item Place
  631. var centerPoint string
  632. err := rows.Scan(&item.ID, &item.PublicID, &item.Code, &item.Name, &item.Region, &item.CoverURL, &item.Description, &centerPoint, &item.Status, &item.CreatedAt, &item.UpdatedAt)
  633. if err != nil {
  634. return nil, fmt.Errorf("scan place row: %w", err)
  635. }
  636. item.CenterPoint = json.RawMessage(centerPoint)
  637. return &item, nil
  638. }
  639. func scanMapAsset(row pgx.Row) (*MapAsset, error) {
  640. var item MapAsset
  641. err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
  642. if errors.Is(err, pgx.ErrNoRows) {
  643. return nil, nil
  644. }
  645. if err != nil {
  646. return nil, fmt.Errorf("scan map asset: %w", err)
  647. }
  648. return &item, nil
  649. }
  650. func scanMapAssetFromRows(rows pgx.Rows) (*MapAsset, error) {
  651. var item MapAsset
  652. err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.LegacyMapID, &item.LegacyMapPublicID, &item.Code, &item.Name, &item.MapType, &item.CoverURL, &item.Description, &item.Status, &item.CurrentTileReleaseID, &item.CreatedAt, &item.UpdatedAt)
  653. if err != nil {
  654. return nil, fmt.Errorf("scan map asset row: %w", err)
  655. }
  656. return &item, nil
  657. }
  658. func scanTileRelease(row pgx.Row) (*TileRelease, error) {
  659. var item TileRelease
  660. var metadataJSON string
  661. err := row.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
  662. if errors.Is(err, pgx.ErrNoRows) {
  663. return nil, nil
  664. }
  665. if err != nil {
  666. return nil, fmt.Errorf("scan tile release: %w", err)
  667. }
  668. item.MetadataJSON = json.RawMessage(metadataJSON)
  669. return &item, nil
  670. }
  671. func scanTileReleaseFromRows(rows pgx.Rows) (*TileRelease, error) {
  672. var item TileRelease
  673. var metadataJSON string
  674. err := rows.Scan(&item.ID, &item.PublicID, &item.MapAssetID, &item.LegacyMapVersionID, &item.LegacyMapVersionPub, &item.VersionCode, &item.Status, &item.TileBaseURL, &item.MetaURL, &item.PublishedAssetRoot, &metadataJSON, &item.PublishedAt, &item.CreatedAt, &item.UpdatedAt)
  675. if err != nil {
  676. return nil, fmt.Errorf("scan tile release row: %w", err)
  677. }
  678. item.MetadataJSON = json.RawMessage(metadataJSON)
  679. return &item, nil
  680. }
  681. func scanCourseSource(row pgx.Row) (*CourseSource, error) {
  682. var item CourseSource
  683. var metadataJSON string
  684. err := row.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
  685. if errors.Is(err, pgx.ErrNoRows) {
  686. return nil, nil
  687. }
  688. if err != nil {
  689. return nil, fmt.Errorf("scan course source: %w", err)
  690. }
  691. item.MetadataJSON = json.RawMessage(metadataJSON)
  692. return &item, nil
  693. }
  694. func scanCourseSourceFromRows(rows pgx.Rows) (*CourseSource, error) {
  695. var item CourseSource
  696. var metadataJSON string
  697. err := rows.Scan(&item.ID, &item.PublicID, &item.LegacyPlayfieldVersionID, &item.LegacyPlayfieldVersionPub, &item.SourceType, &item.FileURL, &item.Checksum, &item.ParserVersion, &item.ImportStatus, &metadataJSON, &item.ImportedAt, &item.CreatedAt, &item.UpdatedAt)
  698. if err != nil {
  699. return nil, fmt.Errorf("scan course source row: %w", err)
  700. }
  701. item.MetadataJSON = json.RawMessage(metadataJSON)
  702. return &item, nil
  703. }
  704. func scanCourseSet(row pgx.Row) (*CourseSet, error) {
  705. var item CourseSet
  706. err := row.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
  707. if errors.Is(err, pgx.ErrNoRows) {
  708. return nil, nil
  709. }
  710. if err != nil {
  711. return nil, fmt.Errorf("scan course set: %w", err)
  712. }
  713. return &item, nil
  714. }
  715. func scanCourseSetFromRows(rows pgx.Rows) (*CourseSet, error) {
  716. var item CourseSet
  717. err := rows.Scan(&item.ID, &item.PublicID, &item.PlaceID, &item.MapAssetID, &item.Code, &item.Mode, &item.Name, &item.Description, &item.Status, &item.CurrentVariantID, &item.CreatedAt, &item.UpdatedAt)
  718. if err != nil {
  719. return nil, fmt.Errorf("scan course set row: %w", err)
  720. }
  721. return &item, nil
  722. }
  723. func scanCourseVariant(row pgx.Row) (*CourseVariant, error) {
  724. var item CourseVariant
  725. var configPatch string
  726. var metadataJSON string
  727. err := row.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
  728. if errors.Is(err, pgx.ErrNoRows) {
  729. return nil, nil
  730. }
  731. if err != nil {
  732. return nil, fmt.Errorf("scan course variant: %w", err)
  733. }
  734. item.ConfigPatch = json.RawMessage(configPatch)
  735. item.MetadataJSON = json.RawMessage(metadataJSON)
  736. return &item, nil
  737. }
  738. func scanCourseVariantFromRows(rows pgx.Rows) (*CourseVariant, error) {
  739. var item CourseVariant
  740. var configPatch string
  741. var metadataJSON string
  742. err := rows.Scan(&item.ID, &item.PublicID, &item.CourseSetID, &item.SourceID, &item.SourcePublicID, &item.Name, &item.RouteCode, &item.Mode, &item.ControlCount, &item.Difficulty, &item.Status, &item.IsDefault, &configPatch, &metadataJSON, &item.CreatedAt, &item.UpdatedAt)
  743. if err != nil {
  744. return nil, fmt.Errorf("scan course variant row: %w", err)
  745. }
  746. item.ConfigPatch = json.RawMessage(configPatch)
  747. item.MetadataJSON = json.RawMessage(metadataJSON)
  748. return &item, nil
  749. }
  750. func scanMapRuntimeBinding(row pgx.Row) (*MapRuntimeBinding, error) {
  751. var item MapRuntimeBinding
  752. err := row.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
  753. if errors.Is(err, pgx.ErrNoRows) {
  754. return nil, nil
  755. }
  756. if err != nil {
  757. return nil, fmt.Errorf("scan runtime binding: %w", err)
  758. }
  759. return &item, nil
  760. }
  761. func scanMapRuntimeBindingFromRows(rows pgx.Rows) (*MapRuntimeBinding, error) {
  762. var item MapRuntimeBinding
  763. err := rows.Scan(&item.ID, &item.PublicID, &item.EventID, &item.EventPublicID, &item.PlaceID, &item.PlacePublicID, &item.MapAssetID, &item.MapAssetPublicID, &item.TileReleaseID, &item.TileReleasePublicID, &item.CourseSetID, &item.CourseSetPublicID, &item.CourseVariantID, &item.CourseVariantPublicID, &item.Status, &item.Notes, &item.CreatedAt, &item.UpdatedAt)
  764. if err != nil {
  765. return nil, fmt.Errorf("scan runtime binding row: %w", err)
  766. }
  767. return &item, nil
  768. }