# [KB] Knowledgebase ROOV ## Radioplus repository development cycle ### Development ```plantuml actor Developer as dev actor Reviewer as rev participant Bitbucket as bitbucket participant Gitlab as gitlab participant "Dev environment" as devenv note over dev origin: git@bitbucket.org:rctiplus/radioplus-webssr.git end note note over rev origin: git@bitbucket.org:rctiplus/radioplus-webssr.git end note dev ->o bitbucket: Push to "development" rev --> rev: Change remote origin url to "gitlab" note right git remote set-url origin https://mnc-repo.mncdigital.com/roov/radioplus.git end note note over rev origin: https://mnc-repo.mncdigital.com/roov/radioplus.git end note rev --> rev: Fetch origin "development" note right git fetch origin development end note rev --> gitlab: Force push to development note right git push -f origin development end note gitlab --> devenv: Deployment to Dev (CI/CD) rev --> rev: Revert back remote origin url to "bitbucket" note right git remote set-url origin git@bitbucket.org:rctiplus/radioplus-webssr.git end note note over rev origin: git@bitbucket.org:rctiplus/radioplus-webssr.git end note ``` ```plantuml @startuml note "moderationStatusID = {\n\tPENDING: 1\n\tAPPROVE: 2\n\tREJECT: 3\n}" as moderationStatus PENDING: 1 APPROVE: 2 REJECT: 3 [*] --> PENDING PENDING --> APPROVE PENDING --> REJECT APPROVE --> PENDING REJECT --> PENDING ``` ## Auth Client Scheme ```plantuml actor Client as client participant AuthAPI as auth group Authenticate client client -> auth: Authenticate client `GET /clients` note right Headers: { "Authrorization": "Basic ${CLIENT_ID}:${CLIENT_SECRET}" } end note auth --> client: Authenticate client responses` note right Headers: `X-Client-Token, X-Client-Token-Expired-At end note end group Authenticate session client -> auth: Authenticate sessions: `POST /sessions` note right Headers: { "Authrorization": "Bearer ${X-Client-Token}" } end note auth --> client: Authenticate session responses note right Headers: `X-Access-Token, X-Access-Token-Expired-At, X-Refresh-Access-Token, X-Refresh-Access-Token-Expired-At` end note end group Refresh session client -> client: X-Acces-Token expired else Refresh session not expired client -> auth: Refresh sessions: `PUT /sessions` note right Headers: { "Authrorization": "Bearer ${X-Refresh-Access-Token}" } end note auth --> client: Authenticate session responses note right Headers: `X-Access-Token, X-Access-Token-Expired-At, X-Refresh-Access-Token, X-Refresh-Access-Token-Expired-At` end note else Refresh token expired client -> client: X-Refresh-Access-Token expired note right Repeat Authenticate session end note end ``` ## Entity Relationship Diagram (ERD) ### SQL Database ```plantuml entity AuthProvider { ID: smallint <<PK>> -- slug: varchar(255) <<UNIQUE>> metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note left metadata = { saltRounds providerURL environment } end note entity ClientType { ID: smallint <<PK>> -- name: varchar(255) createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } entity ClientAuth { ID: smallint <<PK>> -- typeID: smallint clientID: varchar(8) credentials: JSON metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON } note right credentails = { clientSecret clientLifetime } end note entity AdminAuth { ID: bigint <<PK, AI>> -- userID: bigint <<FK>> username: varchar(255) <<UNIQUE>> authProviderID: smallint credential: tinytext <<NULLABLE>> statusID: smallint metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note right metadata = { super: bool } end note ClientAuth "typeID"||--||"ID"ClientType AdminAuth "authProviderID"||--||"ID" AuthProvider note "statusID = {\n\tINACTIVE: 1\n\tACTIVE: 2\n}" as status entity ContentType { ID: smallint <<PK>> -- name: varchar(255) createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note top ContentType = { LIVE: 1 AOD: 2 VOD: 3 } end note entity ContentCategory { ID: smallint <<PK>> -- slug: varchar(255) <<UNIQUE>> name: varchar(255) imageURL: varchar(511) <<NULLABLE>> statusID: smallint metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note top metadata = { description } ContentCategory = [slug][int] { comedy: 1 horror: 2 lifestyle: 4 stories: 5 audio-series: 6 music: 7 business: 9 kids-family: 10 health: 11 sports: 12 religion: 13 audiobook: 14 religion-islam: 15 religion-kristen: 16 religion-buddha = 17 religion-hindu = 18 religion-kong-hu-cu = 19 religion-katolik = 20 video = 21 } end note entity UserAuth { ID: bigint <<PK, AI>> -- extID: bigint <<UNIQUE>> userID: bigint <<FK>> authProviderID: smallint credential: tinytext statusID: smallint metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } entity User { ID: bigint <<PK, AI>> -- email: varchar(255) <<NULLABLE>> fullName: varchar(255) avatarURL: varchar(511) <<NULLABLE>> metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note top metadata = { gender phoneNumber DOB address } end note entity Content { ID: bigint <<PK, AI>> -- extID: varchar(12) <<UNIQUE>> slug: varhcar(255) <<UNIQUE>> name: varchar(255) userID: bigint <<FK>> statusID: smallint typeID: smallint cityID: smallint sort: bigint metadata: JSON highlight: bool createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note right metadata = { description bannerURL thumbnailURL extBannerURL liveMetadata } end note entity ContentData { ID: bigint <<PK, AI>> -- extID: varchar(12) <<UNIQUE>> slug: varhcar(255) <<UNIQUE>> title: varchar(255) contentID: bigint <<FK>> statusID: smallint dataURL: varchar(511) moderationStatusID: smallint sort: bigint highlight: bool metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note right metadata = { description bannerURL thumbnailURL } end note entity ContentHasCategory { ID: bigint <<PK, AI>> -- contentID: bigint <<FK>> categoryID: smallint <<FK>> createdAt: datetime } entity Template { ID: bigint <<PK, AI>> -- name: varchar(255) typeID: smallint statusID: smallint dataURL: varhcar(511) <<NULLABLE>> metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note right typeID = { BACKGROUND-MUSIC: 1 COVER: 2 } end note entity City { ID: smallint <<PK>> -- slug: varhcar(255) <<UNIQUE>> name: varchar(255) createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } User "ID"||--|>"userID" Content ContentCategory "ID"<|--||"categoryID" ContentHasCategory Content "ID"<|--||"contentID" ContentHasCategory ContentType "ID"||--||"typeID"Content Content "ID"||--|>"contentID"ContentData City "ID"||--|| "cityID" Content User "ID" ||--||"userID" UserAuth User "ID" ||--||"userID" AdminAuth @enduml @enduml ``` ```plantuml entity News { ID: bigint <<PK, AI>> -- slug: varchar(255) title: varchar(255) sort: bigint categoryID: smallint content: text statusID: smallint metadata: JSON createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note left metadata = { bannerURL imageURL videoURL embedURL } end note entity NewsCategory { ID: smallint <<PK, AI>> -- slug: varchar(255) <<UNIQUE>> name: varchar(255) imageURL: varchar(511) <<NULLABLE>> statusID: smallint createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } entity NewsTag { ID: smallint <<PK>> -- name: varchar(255) statusID: smallint createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } entity NewsHasTag { ID: bigint <<PK, AI>> -- tagID: smallint newsID: bigint createdAt: datetime } entity Campaign { ID: bigint <<PK, AI>> -- typeID: smallint name: varchar(255) metadata: JSON statusID: smallint sort: bigint createdAt: datetime updatedAt: datetime modifiedBy: JSON version: bigint } note right typeID = { PROMOTION: 1 TOP_ARTIST: 2 } metadata = { bannerURL content } end note News "categoryID"||--||"ID" NewsCategory NewsHasTag "newsID"||--|>"ID" News NewsHasTag "tagID"||--|>"ID" NewsTag ``` ### NoSQL ```plantuml entity Settings { ID: Objecttd -- extID: int key: string description: string value: string | []any isArray: bool createdAt: date updatedAt: date } entity Privilege { ID: ObjectId -- extID: string ability: string createdAt: date updatedAt: date } entity SubjectPrivileges { ID: ObjectId -- subjectExtID: string name: string privilegeExtID: string createdAt: date privilege: Privilege } ``` ## Migrations ```plantuml entity Template {} note right from backsounds: name = backsounds.name statusID = backsounds.active dataURL = backsounds.url metadata = { duration: backsounds.duration } end note ``` ```plantuml entity UserAuth {} note right extID = userdetail.UDSso_id statusID = UDStatus metadata = { SSOUnique: userdetail.UDSsoUnique } end note ``` ```plantuml entity User {} note right email = userdetail.UDEmail fullName = userdetail.UDFirstname+userdetail.UDLastname avatarURL = userdetail.UDUrl_profile metadata = { phoneNumber: userdetail.UDPhoneNumber address: userdetail.UDAddress DOB: userdetail.UDDateOfBirth gender: userdetail.UDGender } end note ``` ```plantuml entity Content {} note right podcasts: slug = kebabCase(podcasts.title) name = podcasts.title userID = podcasts.created_by typeID = AOD statusID = podcasts.status metadata = { description: podcasts.description bannerURL: podcasts.image_banner thumbnailURL: podcasts.image_name } sort = podcasts.sort radios: slug = kebabCase(radios.name) name = radios.name userID = radios.created_by typeID = LIVE statusID = radios.status metadata = { bannerURL: radios.image_banner thumbnailURL: radios.image_name extBannerURL: radios.image_banner_radio_partner liveMetadata: { frequency: radios.frequency city: radios.city address: radios.address geoPoint: { latitude: radios.latitude longitude: radios.longitude } phoneNumber: radios.phone website: radios.website fax: radios.fax email: radios.email socialMedia: { whatsapp: radios.whatsapp facebook: radios.facebook twitter: radios.twitter instagram: radios.instagram } } } sort = radios.sort_network videos: slug = kebabCase(videos.title) name = videos.title userID = videos.created_by typeID = VIDEO statusID = videos.status metadata = { description: videos.description thumbnailURL: videos.image_name } sort = videos.sort spirituals: slug = kebabCase(spirituals.title) name = sprituals.title userID = spirituals.created_by typeID = AOD statusID = spirituals.status metadata = { description: spirituals.description thumbnailURL: spirituals.image_name bannerURL: spirituals.image_banner } sort = spirituals.sort end note ``` ```plantuml entity ContentHasCategory {} note right podcasts: categoryID = podcasts.categoryPodcasts_id sprituals: categoryID = spirituals.category_id radios: categoryID = Music for radios.statusRadios == 'rMusic' end note ``` ```plantuml entity ContentData {} note right content_podcasts: slug = kebabCase(content_podcasts.title) title = content_podcasts.title contentID = content_podcasts.podcasts_id statusID = content_podcasts.status dataURL = content_podcasts.embed_url OR content_podcasts.filename moderationStatusID = content_podcasts.moderation_status metadata = { description: content_podcasts.description thumbnailURL: content_podcasts.image_name bannerURL: content_podcasts.image_url } sort: content_podcasts.sort content_spirituals: slug = kebabCase(content_spirituals.title) title = content_spirituals.title contentID = spritiuals_having_content.content_id == content_spirituals.id statusID = content_spirituals.status dataURL = content_spirituals.embed_url OR content_spirituals.filename moderationStatusID = APPROVE metadata = { thumbnailURL: content_spirituals.image_name bannerURL: content_spirituals.image_banner } radios: slug = kebabCase(radios.name) title = radios.name contentID = Content.slug == kebabCase(radios.name) statusID = radios.status dataURL = radios.audio_url moderationStatusID = APPROVE highlight = radios.statusRadios == 'rBanner' metadata = { bannerURL: radios.image_banner thumbnailURL: radios.image_name } sort: radios.sort_banner || radios.sort_music videos: slug = kebabCase(videos.title) title = videos.title contentID = Content.slug == kebabCase(videos.title) statusID = videos.status dataURL = videos.embed_url OR videos.filename moderationStatusID = APPROVE metadata = { thumbnailURL: videos.image_name } sort: videos.sort end note ``` ```plantuml entity News {} note right news: slug = kebabCase(news.title) title = news.title sort = news.sort category_id = news.category_id content = news.content metadata = { bannerURL: news.image_banner imageURL: news.image_content videoURL: news.videos_content embedURL: news.embed_url } end note ``` ```plantuml entity NewsHasTag {} note right news_having_tag: news_id: news_id == news.id tag_id: tag_id == news_tag.id end note ``` ```plantuml entity Campaign {} note right content_topartists: typeID = TOP_ARTIST name = title sort = sort metadata = { bannerURL: image_banner content: content } statusID = status end note ``` ### Content/Content Data ExtID ```json= CONTENT_extID_PREFIX = { PODCAST: 'P-', RADIO: 'R-', SPIRITUAL: 'S-', VIDEO: 'V-', NEWS: 'N-', } ``` ## Development ### Credentials ```json > CMS App Client credential. ClientId = DIZD9JPG ClientSecret = oatfUPrKt3onkif28103c6DnHGTkyOIo > Web App Client credential. ClientId = ODOLLDGT ClientSecret = YSwDkSK1XikqIFPihyfvj8t9QmmNC8hC > Mobile App Client credential. ClientId = G1UP424T ClientSecret = ptouROa31JE81sacnRZo5x8a2oF5VHqU > H2H Client credential. ClientId = CEP3UDOL ClientSecret = aezeen7chunievoh6ar1mohk0Lae6pah > Super Administrator credential. Username = superadmin Password = 9djqf2MWl29j1jvMr5tHWDfYS4TCYcTb ``` ### Mobile Integration #### Home Curation ```json= VIEW_TYPE = { UNKNOWN: 0, SQUARE_GRID: 1, HORIZONTAL_LIST: 2, TITLE: 3, CAROUSEL: 4, MINI_PLAYER: 5, VERTICAL_LIST: 6, PORTRAIT_GRID: 7, LANDSCAPE_GRID: 8, COMMENT_LIST: 9, BANNER: 10, SPECIAL_BANNER: 11, COMMENT_FORM: 12, LANDSCAPE_TILE: 13 } DATA_TYPE = { OBJECT: 1, ARRAY: 2, STRING: 3, INTEGER: 4, DECIMAL: 5, BOOLEAN: 6, ARRAY_CURRATOR_OBJECT: 7, ARRAY_STRING: 8, ARRAY_INTEGER: 9, ARRAY_DECIMAL: 10, ARRAY_BOOLEAN: 11, } ``` #### API Response Body ```json= BODY = { data: Object meta: Object status: Object } ``` ##### CURRATOR OBJECT ```json= CURRATOR_OBJECT = { viewTypeID, dataTypeID, data, callback: { url, method, query, }, } ``` #### Mobile response convention **These responses is mapped based on VIEW_TYPE** ##### LANDSCAPE | PORTRAIT | SQUARE_GRID ```json= { "ID": number, "title": string, "subtitle": string, "imageURL": string } ``` ###### PORTRAIT GRID ![](https://i.imgur.com/89tVU1y.jpg) ###### LANDSCAPE_GRID ![](https://i.imgur.com/P8TWSVE.png) ###### SQURE_GRID ![](https://i.imgur.com/lO4K7BB.png) ##### SPECIAL_BANNER ```json= { "imageURL": string, "title": string, } ``` ![](https://i.imgur.com/KfHQgWF.jpg) ##### CAROUSEL ```json= { "ID": number, "title": string, "subtitle": string } ``` ![](https://i.imgur.com/smcOPPW.png) ##### MINI_PLAYER ```json= { "ID": number, "dataURL": string, "title": string, "subtitle": string } ``` ![](https://i.imgur.com/txP5Liw.png) ##### BANNER | TITLE `string` on field `data` ###### BANNER ![](https://i.imgur.com/rkRtTM9.jpg) ###### TITLE ![](https://i.imgur.com/Z2bvkh7.png) ##### COMMENT_LIST ```json= { "ID": number, "createdAt": number, "likes": number, "replies": [ "ID": number, "createdAt": number, "user": { "ID": number, "fullName": string, "avatarURL": string }, "message": string ], "user": { "ID": number, "fullName": string, "avatarURL": string }, "message": string } ``` ![](https://i.imgur.com/QH1d3xt.png) ##### VERTICAL_LIST ```json= { "ID": number, "imageURL": string, "title": string, "subtitle": string, "intro": string } ``` ![](https://i.imgur.com/pwNDsQA.jpg) ##### COMMENT_FORM ![](https://i.imgur.com/PY1f126.png) ##### LANDSCAPE_TILE ```json= { "ID": number, "title": string, "imageURL": string } ``` ![](https://i.imgur.com/e1pgaJ1.png)