@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/.cz.toml +1 -1
- package/.eslintignore +1 -0
- package/CHANGELOG.md +22 -0
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/bin/add.ts +70 -35
- package/src/bin/delete.ts +120 -24
- package/src/bin/set.ts +45 -27
- package/src/bin/types_supabase.ts +2867 -0
- package/src/bin/upload.ts +104 -142
- package/src/bin/utils.ts +58 -4
- package/test/test_headers_rls.ts +26 -0
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
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|