diff --git a/src/api/channels.ts b/src/api/channels.ts index b40c6f28..c4f82ee4 100644 --- a/src/api/channels.ts +++ b/src/api/channels.ts @@ -150,12 +150,12 @@ export function displayChannels(data: Channel[]) { } export async function getActiveChannels(supabase: SupabaseClient, appid: string) { - const { data, error: vError } = await supabase - .from('channels') - .select(` + const [{ data, error: vError }, { data: appData, error: appError }] = await Promise.all([ + supabase + .from('channels') + .select(` id, name, - public, allow_emulator, allow_dev, ios, @@ -168,13 +168,24 @@ export async function getActiveChannels(supabase: SupabaseClient, appi app_id, version (id, name) `) - .eq('app_id', appid) - // .eq('created_by', userId) - .order('created_at', { ascending: false }) + .eq('app_id', appid) + .order('created_at', { ascending: false }), - if (vError) { + supabase.from('apps') + .select('default_channel_ios, default_channel_android') + .eq('app_id', appid) + .single(), + ]) + + if (vError || appError) { log.error(`App ${appid} not found in database`) + console.log(vError, appError) program.error('') } - return data as any as Channel[] + return data.map((channel) => { + return { + ...channel, + public: appData?.default_channel_ios === channel.id || appData?.default_channel_android === channel.id, + } + }) as any as Channel[] } diff --git a/src/bundle/upload.ts b/src/bundle/upload.ts index 83703ffe..6ef7cb70 100644 --- a/src/bundle/upload.ts +++ b/src/bundle/upload.ts @@ -472,6 +472,17 @@ async function setVersionInChannel( .rpc('is_allowed_capgkey', { apikey, keymode: ['write', 'all'] }) .single() + const { data: appData, error: appError } = await supabase + .from('apps') + .select('default_channel_ios, default_channel_android') + .eq('app_id', appid) + .single() + + if (appError) { + log.error(`Cannot get app data ${formatError(appError)}`) + program.error('') + } + if (apiAccess) { const { error: dbError3, data } = await updateOrCreateChannel(supabase, { name: channel, @@ -486,7 +497,7 @@ async function setVersionInChannel( } const appidWeb = convertAppName(appid) const bundleUrl = `${localConfig.hostWeb}/app/p/${appidWeb}/channel/${data.id}` - if (data?.public) + if (appData?.default_channel_ios === data.id || appData?.default_channel_android === data.id) log.info('Your update is now available in your public channel 🎉') else if (data?.id) log.info(`Link device to this bundle to try it: ${bundleUrl}`) diff --git a/src/channel/set.ts b/src/channel/set.ts index 68af9ca2..9d6d0b6a 100644 --- a/src/channel/set.ts +++ b/src/channel/set.ts @@ -3,7 +3,7 @@ import type { OptionsBase, } from '../utils' import { exit } from 'node:process' -import { intro, log, outro } from '@clack/prompts' +import { intro, isCancel, log, outro, select } from '@clack/prompts' import { program } from 'commander' import { checkAppExistsAndHasPermissionOrgErr } from '../api/app' import { @@ -128,6 +128,7 @@ export async function setChannel(channel: string, appId: string, options: Option log.info(`Set ${appId} channel: ${channel} to @${bundleVersion}`) channelPayload.version = data.id } + let publicChannel = null as boolean | null if (latestRemote) { const { data, error: vError } = await supabase .from('app_versions') @@ -151,7 +152,7 @@ export async function setChannel(channel: string, appId: string, options: Option } log.info(`Set ${appId} channel: ${channel} to ${state}`) - channelPayload.public = state === 'default' + publicChannel = state === 'default' } if (downgrade != null) { log.info(`Set ${appId} channel: ${channel} to ${downgrade ? 'allow' : 'disallow'} downgrade`) @@ -195,11 +196,106 @@ export async function setChannel(channel: string, appId: string, options: Option log.info(`Set ${appId} channel: ${channel} to ${finalDisableAutoUpdate} disable update strategy to this channel`) } try { - const { error: dbError } = await updateOrCreateChannel(supabase, channelPayload) + const { error: dbError, data: channelData } = await updateOrCreateChannel(supabase, channelPayload) if (dbError) { log.error(`Cannot set channel the upload key is not allowed to do that, use the "all" for this.`) program.error('') } + if (publicChannel != null) { + const { data: appData, error: appError } = await supabase + .from('apps') + .select('default_channel_android, default_channel_ios') + .eq('app_id', appId) + .single() + if (appError) { + log.error(`Cannot get app ${appId}`) + program.error('') + } + if (!publicChannel) { + if (appData?.default_channel_android !== channelData.id && appData?.default_channel_ios !== channelData.id) { + log.info(`Channel ${channel} is not public for both iOS and Android.`) + } + else { + if (appData?.default_channel_android === channelData.id) { + const { error: androidError } = await supabase + .from('apps') + .update({ default_channel_android: null }) + .eq('app_id', appId) + if (androidError) { + log.error(`Cannot set default channel android to null`) + program.error('') + } + } + if (appData?.default_channel_ios === channelData.id) { + const { error: iosError } = await supabase + .from('apps') + .update({ default_channel_ios: null }) + .eq('app_id', appId) + if (iosError) { + log.error(`Cannot set default channel ios to null`) + program.error('') + } + } + if ((appData?.default_channel_ios === null && appData?.default_channel_android === channelData.id) || (appData?.default_channel_ios === channelData.id && appData?.default_channel_android === null)) { + const { error: bothError } = await supabase + .from('apps') + .update({ default_channel_sync: true }) + .eq('app_id', appId) + if (bothError) { + log.error(`Cannot set default channel sync to true`) + program.error('') + } + } + } + } + else if (appData?.default_channel_ios === channelData.id && appData?.default_channel_android === channelData.id) { + // check if pehaps the channel is already public + log.info(`Channel ${channel} is already public for both iOS and Android.`) + } + else { + // here we need to ask the user if he wants the channel to become public for iOS android or Both + const platformType = await select({ + message: 'Do you want the channel to become public for iOS android or Both?', + options: [ + { value: 'iOS', label: 'iOS' }, + { value: 'Android', label: 'Android' }, + { value: 'Both', label: 'Both' }, + ], + }) + if (isCancel(platformType)) { + outro(`Bye 👋`) + exit() + } + + const platform = platformType as 'iOS' | 'Android' | 'Both' + if (platform === 'iOS' || platform === 'Android') { + const opositePlatform = platform === 'iOS' ? 'android' : 'ios' + const { error: singlePlatformError } = await supabase + .from('apps') + .update({ [`default_channel_${platform.toLowerCase()}`]: channelData.id, default_channel_sync: appData?.[`default_channel_${opositePlatform}`] === channelData.id }) + .eq('app_id', appId) + if (singlePlatformError) { + log.error(`Failed to set default channel ${platform} to ${channel}.`) + log.error(`This may be due to insufficient permissions or a database error.${formatError(singlePlatformError)}`) + program.error('') + } + } + else { + const { error: bothPlatformError } = await supabase + .from('apps') + .update({ default_channel_sync: true, default_channel_ios: channelData.id, default_channel_android: channelData.id }) + .eq('app_id', appId) + if (bothPlatformError) { + log.error(`Failed to synchronize default channel settings across both platforms.`) + log.error(`Unable to set channel '${channel}' as default for both iOS and Android.${formatError(bothPlatformError)}`) + program.error('') + } + } + } + if (publicChannel && (appData?.default_channel_ios !== channelData.id || appData?.default_channel_android !== channelData.id)) { + log.info(`Set ${appId} channel: ${channel} to ${publicChannel ? 'public' : 'private'}`) + } + } } catch { log.error(`Cannot set channel the upload key is not allowed to do that, use the "all" for this.`) @@ -217,7 +313,8 @@ export async function setChannel(channel: string, appId: string, options: Option }).catch() } catch (err) { - log.error(`Unknow error ${formatError(err)}`) + log.error(`An unexpected error occurred while setting channel '${channel}' for app '${appId}'.`) + log.error(`Please verify your inputs and try again.${formatError(err)}`) program.error('') } outro(`Done ✅`) diff --git a/src/types/supabase.types.ts b/src/types/supabase.types.ts index 273554d8..6a3a8184 100644 --- a/src/types/supabase.types.ts +++ b/src/types/supabase.types.ts @@ -7,6 +7,31 @@ export type Json = | Json[] export interface Database { + graphql_public: { + Tables: { + [_ in never]: never + } + Views: { + [_ in never]: never + } + Functions: { + graphql: { + Args: { + operationName?: string + query?: string + variables?: Json + extensions?: Json + } + Returns: Json + } + } + Enums: { + [_ in never]: never + } + CompositeTypes: { + [_ in never]: never + } + } public: { Tables: { apikeys: { @@ -205,6 +230,9 @@ export interface Database { Row: { app_id: string created_at: string | null + default_channel_android: number | null + default_channel_ios: number | null + default_channel_sync: boolean default_upload_channel: string icon_url: string id: string | null @@ -219,6 +247,9 @@ export interface Database { Insert: { app_id: string created_at?: string | null + default_channel_android?: number | null + default_channel_ios?: number | null + default_channel_sync?: boolean default_upload_channel?: string icon_url: string id?: string | null @@ -233,6 +264,9 @@ export interface Database { Update: { app_id?: string created_at?: string | null + default_channel_android?: number | null + default_channel_ios?: number | null + default_channel_sync?: boolean default_upload_channel?: string icon_url?: string id?: string | null @@ -245,6 +279,20 @@ export interface Database { user_id?: string | null } Relationships: [ + { + foreignKeyName: 'apps_default_channel_android_fkey' + columns: ['default_channel_android'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, + { + foreignKeyName: 'apps_default_channel_ios_fkey' + columns: ['default_channel_ios'] + isOneToOne: false + referencedRelation: 'channels' + referencedColumns: ['id'] + }, { foreignKeyName: 'apps_user_id_fkey' columns: ['user_id'] @@ -517,7 +565,7 @@ export interface Database { } Insert: { created_at?: string | null - email?: string + email: string id?: string } Update: { @@ -527,6 +575,30 @@ export interface Database { } Relationships: [] } + deleted_apps: { + Row: { + app_id: string + created_at: string | null + deleted_at: string | null + id: number + owner_org: string + } + Insert: { + app_id: string + created_at?: string | null + deleted_at?: string | null + id?: number + owner_org: string + } + Update: { + app_id?: string + created_at?: string | null + deleted_at?: string | null + id?: number + owner_org?: string + } + Relationships: [] + } deploy_history: { Row: { app_id: string @@ -885,6 +957,7 @@ export interface Database { storage_unit: number | null stripe_id: string updated_at: string + version: number } Insert: { bandwidth: number @@ -907,6 +980,7 @@ export interface Database { storage_unit?: number | null stripe_id?: string updated_at?: string + version?: number } Update: { bandwidth?: number @@ -929,6 +1003,7 @@ export interface Database { storage_unit?: number | null stripe_id?: string updated_at?: string + version?: number } Relationships: [] } @@ -1163,10 +1238,6 @@ export interface Database { } Returns: string } - calculate_daily_app_usage: { - Args: Record - Returns: undefined - } check_min_rights: | { Args: { @@ -1242,10 +1313,6 @@ export interface Database { Args: Record Returns: number } - count_all_paying: { - Args: Record - Returns: number - } count_all_plans_v2: { Args: Record Returns: { @@ -1328,6 +1395,19 @@ export interface Database { uninstall: number }[] } + | { + Args: { + p_org_id: string + p_start_date: string + p_end_date: string + } + Returns: { + mau: number + bandwidth: number + storage: number + deleted_apps: number + }[] + } get_app_versions: { Args: { appid: string @@ -1377,19 +1457,6 @@ export interface Database { subscription_anchor_end: string }[] } - get_daily_version: { - Args: { - app_id_param: string - start_date_param?: string - end_date_param?: string - } - Returns: { - date: string - app_id: string - version_id: number - percent: number - }[] - } get_db_url: { Args: Record Returns: string @@ -1466,18 +1533,6 @@ export interface Database { } Returns: string } - get_infos: { - Args: { - appid: string - deviceid: string - versionname: string - } - Returns: { - current_version_id: number - versiondata: Json - channel: Json - }[] - } get_metered_usage: | { Args: Record @@ -1705,20 +1760,6 @@ export interface Database { uninstall: number }[] } - get_total_storage_size: - | { - Args: { - appid: string - } - Returns: number - } - | { - Args: { - userid: string - appid: string - } - Returns: number - } get_total_storage_size_org: { Args: { org_id: string @@ -1829,14 +1870,6 @@ export interface Database { } Returns: number } - http_post_helper_preprod: { - Args: { - function_name: string - function_type: string - body: Json - } - Returns: number - } invite_user_to_org: { Args: { email: string @@ -2112,32 +2145,27 @@ export interface Database { uninstall: number }[] } - reset_and_seed_app_data: - | { - Args: { - p_app_id: string - } - Returns: undefined - } - | { - Args: { - p_app_id: string - } - Returns: undefined + replicate_to_d1: { + Args: { + record: Json + old_record: Json + operation: string + table_name: string } - reset_and_seed_app_stats_data: - | { - Args: { - p_app_id: string - } - Returns: undefined + Returns: undefined + } + reset_and_seed_app_data: { + Args: { + p_app_id: string } - | { - Args: { - p_app_id: string - } - Returns: undefined + Returns: undefined + } + reset_and_seed_app_stats_data: { + Args: { + p_app_id: string } + Returns: undefined + } reset_and_seed_data: { Args: Record Returns: undefined @@ -2186,33 +2214,6 @@ export interface Database { } Returns: undefined } - update_app_usage: - | { - Args: Record - Returns: undefined - } - | { - Args: { - minutes_interval: number - } - Returns: undefined - } - update_notification: { - Args: { - p_event: string - p_uniq_id: string - p_owner_org: string - } - Returns: undefined - } - upsert_notification: { - Args: { - p_event: string - p_uniq_id: string - p_owner_org: string - } - Returns: undefined - } verify_mfa: { Args: Record Returns: boolean @@ -2220,10 +2221,8 @@ export interface Database { } Enums: { action_type: 'mau' | 'storage' | 'bandwidth' - app_mode: 'prod' | 'dev' | 'livereload' disable_update: 'major' | 'minor' | 'patch' | 'version_number' | 'none' key_mode: 'read' | 'write' | 'all' | 'upload' - pay_as_you_go_type: 'base' | 'units' platform_os: 'ios' | 'android' stats_action: | 'delete' @@ -2279,7 +2278,7 @@ export interface Database { | 'failed' | 'deleted' | 'canceled' - usage_mode: '5min' | 'day' | 'month' | 'cycle' | 'last_saved' + usage_mode: 'last_saved' | '5min' | 'day' | 'cycle' user_min_right: | 'invite_read' | 'invite_upload' @@ -2300,9 +2299,6 @@ export interface Database { s3_path: string | null file_hash: string | null } - match_plan: { - name: string | null - } orgs_table: { id: string | null created_by: string | null diff --git a/src/utils.ts b/src/utils.ts index 1c6496e5..07e856c7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -578,7 +578,7 @@ async function* getFiles(dir: string): AsyncGenerator { && !dirent.name.startsWith('node_modules') && !dirent.name.startsWith('dist') ) { - yield * getFiles(res) + yield* getFiles(res) } else { yield res @@ -784,7 +784,7 @@ async function* walkDirectory(dir: string): AsyncGenerator { for (const entry of entries) { const fullPath = join(dir, entry.name) if (entry.isDirectory()) { - yield * walkDirectory(fullPath) + yield* walkDirectory(fullPath) } else { yield fullPath