@capgo/cli 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bin/upload.ts CHANGED
@@ -1,66 +1,8 @@
1
1
  import AdmZip from 'adm-zip';
2
- import axios, { AxiosResponse } from 'axios';
3
- import prettyjson from 'prettyjson';
4
2
  import { program } from 'commander';
3
+ import { randomUUID } from 'crypto';
5
4
  import cliProgress from 'cli-progress';
6
- import axiosRetry from 'axios-retry';
7
- import { host, hostWeb, supaAnon, hostSupa, getConfig } from './utils';
8
-
9
- axiosRetry(axios, { retries: 5, retryDelay: axiosRetry.exponentialDelay });
10
- const oneMb = 1048576; // size of one mb in bytes
11
- const maxMb = 30;
12
- const alertMb = 25;
13
- // enum string format
14
- enum UploadMode {
15
- uft8 = 'utf8',
16
- base64 = 'base64',
17
- hex = 'hex',
18
- binary = 'binary'
19
- }
20
- interface uploadPayload {
21
- version: string
22
- appid: string
23
- fileName?: string
24
- channel: string
25
- format: UploadMode
26
- app: string,
27
- isMultipart: boolean,
28
- chunk: number,
29
- totalChunks: number,
30
- }
31
- interface uploadExternal {
32
- version: string
33
- appid: string
34
- channel: string
35
- external: string
36
- }
37
-
38
- interface ResApi {
39
- fileName: string
40
- }
41
-
42
- const formatDefault = UploadMode.binary;
43
- const chuckNumber = (l: number, divider: number) => l < divider ? 1 : Math.floor(l / divider)
44
- const chuckSize = (l: number, divider: number) => Math.floor(l / chuckNumber(l, divider))
45
-
46
- const mbConvert = {
47
- 'base64': (l: number) => Math.floor((l * 4) / 3),
48
- 'hex': (l: number) => Math.floor(l * 2),
49
- 'binary': (l: number) => l,
50
- 'utf8': (l: number) => l,
51
- }
52
-
53
- const sendToBack = async (data: uploadPayload | uploadExternal, apikey: string): Promise<AxiosResponse<ResApi>> => axios({
54
- method: 'POST',
55
- url: `${hostSupa}/upload`,
56
- data,
57
- validateStatus: () => true,
58
- headers: {
59
- 'Content-Type': 'application/json',
60
- 'apikey': apikey,
61
- authorization: `Bearer ${supaAnon}`
62
- }
63
- })
5
+ import { host, hostWeb, getConfig, createSupabaseClient, updateOrCreateChannel, updateOrCreateVersion, formatError } from './utils';
64
6
 
65
7
  interface Options {
66
8
  version: string
@@ -68,17 +10,16 @@ interface Options {
68
10
  apikey: string
69
11
  channel?: string
70
12
  external?: string
71
- format?: UploadMode
72
13
  }
14
+
15
+ const maxMb = 30;
16
+ const alertMb = 25;
17
+
73
18
  export const uploadVersion = async (appid: string, options: Options) => {
74
19
  let { version, path, channel } = options;
75
- const { apikey, external, format } = options;
20
+ const { apikey, external } = options;
76
21
  channel = channel || 'dev';
77
22
  const config = await getConfig();
78
- let formatType = formatDefault;
79
- if (format && format in UploadMode) {
80
- formatType = format;
81
- }
82
23
  appid = appid || config?.app?.appId
83
24
  version = version || config?.app?.package?.version
84
25
  path = path || config?.app?.webDir
@@ -89,85 +30,106 @@ export const uploadVersion = async (appid: string, options: Options) => {
89
30
  program.error("Missing argument, you need to provide a appid and a version and a path, or be in a capacitor project");
90
31
  }
91
32
  console.log(`Upload ${appid}@${version} started from path "${path}" to Capgo cloud`);
92
- if (external) {
93
- try {
94
- const res = await sendToBack({
95
- version,
96
- appid,
97
- channel,
98
- external
99
- }, apikey)
100
- if (res.status !== 200) {
101
- program.error(`Server Error \n${prettyjson.render(res?.data || "")}`);
102
- }
103
- } catch (err) {
104
- if (axios.isAxiosError(err) && err.response) {
105
- program.error(`Network Error \n${prettyjson.render(err.response?.data)}`);
106
- } else {
107
- program.error(`Unknow error \n${prettyjson.render(err)}`);
108
- }
33
+
34
+ const supabase = createSupabaseClient(apikey)
35
+ const multibar = new cliProgress.MultiBar({
36
+ clearOnComplete: false,
37
+ hideCursor: true
38
+ }, cliProgress.Presets.shades_grey);
39
+
40
+ // add bars
41
+ const b1 = multibar.create(7, 0, {
42
+ format: 'Uploading: [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} Part'
43
+ }, cliProgress.Presets.shades_grey);
44
+ b1.start(7, 0, {
45
+ speed: "N/A"
46
+ });
47
+ // checking if user has access rights before uploading
48
+ const { data: apiAccess, error: apiAccessError } = await supabase
49
+ .rpc('is_allowed_capgkey', { apikey, keymode: ['upload', 'write', 'all'], app_id: appid })
50
+
51
+ if (!apiAccess || apiAccessError) {
52
+ multibar.stop()
53
+ program.error(`Invalid API key or insufisant rights ${formatError(apiAccessError)}`);
54
+ }
55
+ b1.increment();
56
+
57
+ const { data, error: userIdError } = await supabase
58
+ .rpc<string>('get_user_id', { apikey })
59
+
60
+ const userId = data ? data.toString() : '';
61
+ if (!userId || userIdError) {
62
+ multibar.stop()
63
+ program.error(`Cannot verify user ${formatError(userIdError)}`);
64
+ }
65
+ b1.increment();
66
+
67
+ const { data: app, error: dbError0 } = await supabase
68
+ .rpc<string>('exist_app', { appid, apikey })
69
+ if (!app || dbError0) {
70
+ multibar.stop()
71
+ program.error(`Cannot find app ${appid} in your account \n${formatError(dbError0)}`)
72
+ }
73
+ b1.increment();
74
+
75
+ if (!external) {
76
+ const zip = new AdmZip();
77
+ zip.addLocalFolder(path);
78
+ const zipped = zip.toBuffer();
79
+ const mbSize = Math.floor(zipped.byteLength / 1024 / 1024);
80
+ const filePath = `apps/${userId}/${appid}/versions`
81
+ const fileName = randomUUID()
82
+ b1.increment();
83
+ if (mbSize > maxMb) {
84
+ multibar.stop()
85
+ program.error(`The app is too big, the limit is ${maxMb} Mb, your is ${mbSize} Mb`);
109
86
  }
110
- } else {
111
- const b1 = new cliProgress.SingleBar({
112
- format: 'Uploading: [{bar}] {percentage}% | ETA: {eta}s | {value}/{total} Mb'
113
- }, cliProgress.Presets.shades_grey);
114
- try {
115
- const zip = new AdmZip();
116
- zip.addLocalFolder(path);
117
- const zipped = zip.toBuffer();
118
- const appData = zipped.toString(formatType);
119
- // split appData in chunks and send them sequentially with axios
120
- // console.log('appData size', appData.length)
121
- const zippedSize = appData.length;
122
- const mbSize = Math.floor(zippedSize / mbConvert[formatType](oneMb));
123
- // console.log('mbSize', zippedSize, mbSize, mbConvert[formatType](oneMb))
124
- const chunkSize = chuckNumber(zippedSize, mbConvert[formatType](oneMb)) > 1
125
- ? chuckSize(zippedSize, mbConvert[formatType](oneMb)) : zippedSize;
126
- if (mbSize > maxMb) {
127
- program.error(`The app is too big, the limit is ${maxMb} Mb, your is ${mbSize} Mb`);
128
- }
129
- if (mbSize > alertMb) {
130
- console.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb`);
131
- }
132
- const chunks = [];
133
- for (let i = 0; i < appData.length; i += chunkSize) {
134
- chunks.push(appData.slice(i, i + chunkSize));
135
- }
136
- b1.start(chunks.length, 0, {
137
- speed: "N/A"
138
- });
139
- let fileName
140
- for (let i = 0; i < chunks.length; i += 1) {
141
- const response = await sendToBack({
142
- version,
143
- appid,
144
- fileName,
145
- channel,
146
- format: formatType,
147
- app: chunks[i],
148
- isMultipart: chunks.length > 1,
149
- chunk: i + 1,
150
- totalChunks: chunks.length,
151
- }, apikey)
152
- if (response.status !== 200 || !response.data.fileName) {
153
- b1.stop();
154
- program.error(`Server Error \n${prettyjson.render(response?.data || "")}`);
155
- }
156
- b1.update(i + 1)
157
- const data: ResApi = response.data as ResApi
158
- fileName = data.fileName
159
- }
160
- b1.stop();
161
- } catch (err) {
162
- b1.stop();
163
- if (axios.isAxiosError(err) && err.response) {
164
- program.error(`Network Error \n${prettyjson.render(err.response?.data)}`);
165
- } else {
166
- program.error(`Unknow error \n${prettyjson.render(err)}`);
167
- }
87
+ if (mbSize > alertMb) {
88
+ multibar.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb\n`);
168
89
  }
169
- }
170
90
 
91
+ const { error: upError } = await supabase.storage
92
+ .from(filePath)
93
+ .upload(fileName, zipped, {
94
+ contentType: 'application/zip',
95
+ })
96
+ if (upError) {
97
+ multibar.stop()
98
+ program.error(`Cannot upload ${formatError(upError)}`)
99
+ }
100
+ } else if (external && !external.startsWith('https://')) {
101
+ multibar.stop()
102
+ program.error(`External link should should start with "https://" current is "${external}"`)
103
+ }
104
+ b1.increment();
105
+ const fileName = randomUUID()
106
+ const { data: versionData, error: dbError } = await updateOrCreateVersion(supabase, {
107
+ bucket_id: external ? undefined : fileName,
108
+ user_id: userId,
109
+ name: version,
110
+ app_id: appid,
111
+ external_url: external,
112
+ }, apikey)
113
+ if (dbError) {
114
+ multibar.stop()
115
+ program.error(`Cannot add version ${formatError(dbError)}`)
116
+ }
117
+ b1.increment();
118
+ if (versionData && versionData.length) {
119
+ const { error: dbError3 } = await updateOrCreateChannel(supabase, {
120
+ name: channel,
121
+ app_id: appid,
122
+ created_by: userId,
123
+ version: versionData[0].id,
124
+ }, apikey)
125
+ if (dbError3) {
126
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
127
+ }
128
+ } else {
129
+ multibar.log('Cannot set version with upload key, use key with more rights for that\n');
130
+ }
131
+ b1.increment();
132
+ multibar.stop()
171
133
  console.log("App uploaded to server")
172
134
  console.log(`Try it in mobile app: ${host}/app_mobile`)
173
135
  console.log(`Or set the channel ${channel} as public here: ${hostWeb}/app/package/${appid}`)
package/src/bin/utils.ts CHANGED
@@ -1,19 +1,30 @@
1
1
  import { loadConfig } from '@capacitor/cli/dist/config';
2
2
  import { program } from 'commander';
3
+ import { createClient, SupabaseClient } from '@supabase/supabase-js'
4
+ import prettyjson from 'prettyjson';
5
+ import { definitions } from './types_supabase';
3
6
 
4
7
  export const host = 'https://capgo.app';
5
8
  export const hostWeb = 'https://web.capgo.app';
6
- export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
7
- // export const hostSupa = 'https://aucsybvnhavogdmzwtcw.functions.supabase.co';
9
+ // export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
10
+ export const hostSupa = 'https://aucsybvnhavogdmzwtcw.supabase.co';
8
11
 
9
12
  // For local test purposes
10
13
  // export const host = 'http://localhost:3334';
11
14
 
12
15
  /* eslint-disable */
13
- export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
14
- // export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTE4NDE4MjMsImV4cCI6MTk2NzQxNzgyM30.AYzjGigPxlTw4eEkCjoGfYph8WRU3QXgIDcMWptQyAQ'
16
+ // export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
17
+ export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTM1ODcxNjgsImV4cCI6MTk2OTE2MzE2OH0.8FKKJqiGgoVA3p9GH5wvnbWkWywIxVLqQyZFhupZ7C4'
15
18
  /* eslint-enable */
16
19
 
20
+ export const createSupabaseClient = (apikey: string) => createClient(hostSupa, supaAnon, {
21
+ headers: {
22
+ capgkey: apikey,
23
+ }
24
+ })
25
+
26
+ export const formatError = (error: any) => error ? `\n${prettyjson.render(error)}` : ''
27
+
17
28
  interface Config {
18
29
  app: {
19
30
  appId: string;
@@ -32,4 +43,47 @@ export const getConfig = async () => {
32
43
  program.error("No capacitor config file found, run `cap init` first");
33
44
  }
34
45
  return config;
46
+ }
47
+
48
+ export const updateOrCreateVersion = async (supabase: SupabaseClient, update: Partial<definitions['app_versions']>, apikey: string) => {
49
+ // console.log('updateOrCreateVersion', update, apikey)
50
+ const { data, error } = await supabase
51
+ .rpc<string>('exist_app_versions', { appid: update.app_id, name_version: update.name, apikey })
52
+ if (data && !error) {
53
+ update.deleted = false
54
+ return supabase
55
+ .from<definitions['app_versions']>('app_versions')
56
+ .update(update, { returning: "minimal" })
57
+ .eq('app_id', update.app_id)
58
+ .eq('name', update.name)
59
+ }
60
+ // console.log('create Version', data, error)
61
+
62
+ return supabase
63
+ .from<definitions['app_versions']>('app_versions')
64
+ .insert(update, { returning: "minimal" })
65
+
66
+ }
67
+
68
+ export const updateOrCreateChannel = async (supabase: SupabaseClient, update: Partial<definitions['channels']>, apikey: string) => {
69
+ // console.log('updateOrCreateChannel', update)
70
+ if (!update.app_id || !update.name || !update.created_by) {
71
+ console.error('missing app_id, name, or created_by')
72
+ return Promise.reject(new Error('missing app_id, name, or created_by'))
73
+ }
74
+ const { data, error } = await supabase
75
+ .rpc<string>('exist_channel', { appid: update.app_id, name_channel: update.name, apikey })
76
+ if (data && !error) {
77
+ return supabase
78
+ .from<definitions['channels']>('channels')
79
+ .update(update, { returning: "minimal" })
80
+ .eq('app_id', update.app_id)
81
+ .eq('name', update.name)
82
+ .eq('created_by', update.created_by)
83
+ }
84
+ // console.log('create Channel', data, error)
85
+
86
+ return supabase
87
+ .from<definitions['channels']>('channels')
88
+ .insert(update, { returning: "minimal" })
35
89
  }
@@ -0,0 +1,26 @@
1
+ import { createClient } from '@supabase/supabase-js'
2
+
3
+ const supaUrl = 'https://aucsybvnhavogdmzwtcw.supabase.co'
4
+ const apikey = '***'
5
+ const anonKey = '***'
6
+ const init = async () => {
7
+ const supabase = createClient(supaUrl, anonKey, {
8
+ headers: {
9
+ capgkey: '***',
10
+ }
11
+ })
12
+ const { data: userId } = await supabase
13
+ .rpc('get_user_id', { apikey })
14
+ console.log('userId', userId)
15
+ const apps = await supabase.from('apps')
16
+ .select()
17
+ .eq('app_id', 'ee.forgr.captime')
18
+ console.log('apps', apps.data)
19
+ // try to find one app
20
+ }
21
+
22
+ init()
23
+ // (is_app_shared(uid(), app_id) OR is_allowed_apikey((((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text))::character varying, '{read}'::key_mode[]))
24
+ // is_allowed_apikey(((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text), '{read}'::key_mode[])
25
+
26
+ // (((current_setting('request.headers'::text, true))::json ->> 'capgkey'::text) = 'dKC9teP4zHh7Lr5ak7hErgYn385C'::text)