@capgo/cli 0.8.5 → 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/.eslintrc +0 -1
- package/CHANGELOG.md +39 -0
- package/dist/index.js +1 -1
- package/package.json +6 -3
- package/src/bin/add.ts +88 -34
- package/src/bin/delete.ts +130 -30
- package/src/bin/set.ts +57 -31
- package/src/bin/types.d.ts +6 -0
- package/src/bin/types_supabase.ts +2867 -0
- package/src/bin/upload.ts +108 -146
- package/src/bin/utils.ts +82 -5
- package/test/test_headers_rls.ts +26 -0
- package/tsconfig.json +4 -1
package/src/bin/upload.ts
CHANGED
|
@@ -1,75 +1,25 @@
|
|
|
1
|
-
import { loadConfig } from '@capacitor/cli/dist/config';
|
|
2
1
|
import AdmZip from 'adm-zip';
|
|
3
|
-
import axios from 'axios';
|
|
4
|
-
import prettyjson from 'prettyjson';
|
|
5
2
|
import { program } from 'commander';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
6
4
|
import cliProgress from 'cli-progress';
|
|
7
|
-
import
|
|
8
|
-
import { host, hostWeb, hostUpload, supaAnon } from './utils';
|
|
5
|
+
import { host, hostWeb, getConfig, createSupabaseClient, updateOrCreateChannel, updateOrCreateVersion, formatError } from './utils';
|
|
9
6
|
|
|
10
|
-
|
|
11
|
-
const oneMb = 1048576; // size of one mb in bytes
|
|
12
|
-
const maxMb = 30;
|
|
13
|
-
const alertMb = 25;
|
|
14
|
-
// enum string format
|
|
15
|
-
enum UploadMode {
|
|
16
|
-
uft8 = 'utf8',
|
|
17
|
-
base64 = 'base64',
|
|
18
|
-
hex = 'hex',
|
|
19
|
-
binary = 'binary'
|
|
20
|
-
}
|
|
21
|
-
interface uploadPayload {
|
|
7
|
+
interface Options {
|
|
22
8
|
version: string
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
channel
|
|
26
|
-
|
|
27
|
-
app: string,
|
|
28
|
-
isMultipart: boolean,
|
|
29
|
-
chunk: number,
|
|
30
|
-
totalChunks: number,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const formatDefault = UploadMode.binary;
|
|
34
|
-
const chuckNumber = (l: number, divider: number) => l < divider ? l : Math.floor(l / divider)
|
|
35
|
-
const chuckSize = (l: number, divider: number) => Math.floor(l / chuckNumber(l, divider))
|
|
36
|
-
|
|
37
|
-
const mbConvert = {
|
|
38
|
-
'base64': (l: number) => Math.floor((l * 4) / 3),
|
|
39
|
-
'hex': (l: number) => Math.floor(l * 2),
|
|
40
|
-
'binary': (l: number) => l,
|
|
41
|
-
'utf8': (l: number) => l,
|
|
9
|
+
path: string
|
|
10
|
+
apikey: string
|
|
11
|
+
channel?: string
|
|
12
|
+
external?: string
|
|
42
13
|
}
|
|
43
14
|
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
method: 'POST',
|
|
47
|
-
url: hostUpload,
|
|
48
|
-
data,
|
|
49
|
-
validateStatus: () => true,
|
|
50
|
-
headers: {
|
|
51
|
-
'Content-Type': 'application/json',
|
|
52
|
-
'apikey': apikey,
|
|
53
|
-
authorization: `Bearer ${supaAnon}`
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
return res
|
|
57
|
-
}
|
|
15
|
+
const maxMb = 30;
|
|
16
|
+
const alertMb = 25;
|
|
58
17
|
|
|
59
|
-
export const uploadVersion = async (appid, options) => {
|
|
18
|
+
export const uploadVersion = async (appid: string, options: Options) => {
|
|
60
19
|
let { version, path, channel } = options;
|
|
61
|
-
const { apikey, external
|
|
20
|
+
const { apikey, external } = options;
|
|
62
21
|
channel = channel || 'dev';
|
|
63
|
-
|
|
64
|
-
let formatType = formatDefault;
|
|
65
|
-
if (format in UploadMode) {
|
|
66
|
-
formatType = format as UploadMode;
|
|
67
|
-
}
|
|
68
|
-
try {
|
|
69
|
-
config = await loadConfig();
|
|
70
|
-
} catch (err) {
|
|
71
|
-
program.error("No capacitor config file found, run `cap init` first");
|
|
72
|
-
}
|
|
22
|
+
const config = await getConfig();
|
|
73
23
|
appid = appid || config?.app?.appId
|
|
74
24
|
version = version || config?.app?.package?.version
|
|
75
25
|
path = path || config?.app?.webDir
|
|
@@ -80,94 +30,106 @@ export const uploadVersion = async (appid, options) => {
|
|
|
80
30
|
program.error("Missing argument, you need to provide a appid and a version and a path, or be in a capacitor project");
|
|
81
31
|
}
|
|
82
32
|
console.log(`Upload ${appid}@${version} started from path "${path}" to Capgo cloud`);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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`);
|
|
86
|
+
}
|
|
87
|
+
if (mbSize > alertMb) {
|
|
88
|
+
multibar.log(`WARNING !!\nThe app size is ${mbSize} Mb, the limit is ${maxMb} Mb\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { error: upError } = await supabase.storage
|
|
92
|
+
.from(filePath)
|
|
93
|
+
.upload(fileName, zipped, {
|
|
94
|
+
contentType: 'application/zip',
|
|
100
95
|
})
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
} catch (err) {
|
|
105
|
-
if (err.response) {
|
|
106
|
-
program.error(`Network Error \n${prettyjson.render(err.response?.data || "")}`)
|
|
107
|
-
} else {
|
|
108
|
-
program.error(`Network Error \n${prettyjson.render(err || "")}`)
|
|
109
|
-
}
|
|
96
|
+
if (upError) {
|
|
97
|
+
multibar.stop()
|
|
98
|
+
program.error(`Cannot upload ${formatError(upError)}`)
|
|
110
99
|
}
|
|
111
|
-
} else {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
speed: "N/A"
|
|
139
|
-
});
|
|
140
|
-
let fileName
|
|
141
|
-
for (let i = 0; i < chunks.length; i += 1) {
|
|
142
|
-
const res = await sendToBack({
|
|
143
|
-
version,
|
|
144
|
-
appid,
|
|
145
|
-
fileName,
|
|
146
|
-
channel,
|
|
147
|
-
format: formatType,
|
|
148
|
-
app: chunks[i],
|
|
149
|
-
isMultipart: chunks.length > 1,
|
|
150
|
-
chunk: i + 1,
|
|
151
|
-
totalChunks: chunks.length,
|
|
152
|
-
}, apikey)
|
|
153
|
-
if (res.status !== 200) {
|
|
154
|
-
b1.stop();
|
|
155
|
-
program.error(`Server Error \n${prettyjson.render(res?.data || "")}`);
|
|
156
|
-
}
|
|
157
|
-
b1.update(i + 1)
|
|
158
|
-
fileName = res.data.fileName
|
|
159
|
-
}
|
|
160
|
-
b1.stop();
|
|
161
|
-
} catch (err) {
|
|
162
|
-
b1.stop();
|
|
163
|
-
if (err.response) {
|
|
164
|
-
program.error(`Network Error \n${prettyjson.render(err.response?.data || "")}`)
|
|
165
|
-
} else {
|
|
166
|
-
program.error(`Network Error \n${prettyjson.render(err || "")}`)
|
|
167
|
-
}
|
|
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');
|
|
168
127
|
}
|
|
128
|
+
} else {
|
|
129
|
+
multibar.log('Cannot set version with upload key, use key with more rights for that\n');
|
|
169
130
|
}
|
|
170
|
-
|
|
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,12 +1,89 @@
|
|
|
1
|
-
|
|
1
|
+
import { loadConfig } from '@capacitor/cli/dist/config';
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import { createClient, SupabaseClient } from '@supabase/supabase-js'
|
|
4
|
+
import prettyjson from 'prettyjson';
|
|
5
|
+
import { definitions } from './types_supabase';
|
|
6
|
+
|
|
2
7
|
export const host = 'https://capgo.app';
|
|
3
8
|
export const hostWeb = 'https://web.capgo.app';
|
|
4
|
-
export const
|
|
9
|
+
// export const hostSupa = 'https://xvwzpoazmxkqosrdewyv.functions.supabase.co';
|
|
10
|
+
export const hostSupa = 'https://aucsybvnhavogdmzwtcw.supabase.co';
|
|
11
|
+
|
|
5
12
|
// For local test purposes
|
|
6
13
|
// export const host = 'http://localhost:3334';
|
|
7
|
-
// export const hostUpload = 'http://localhost:54321/functions/v1/upload';
|
|
8
|
-
/* eslint-enable */
|
|
9
14
|
|
|
10
15
|
/* eslint-disable */
|
|
11
|
-
export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
|
|
16
|
+
// export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlhdCI6MTYzNzgwNTAwOSwiZXhwIjoxOTUzMzgxMDA5fQ.8tgID1d4jodPwuo_fz4KHN4o1XKB9fnqyt0_GaJSj-w'
|
|
17
|
+
export const supaAnon = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImF1Y3N5YnZuaGF2b2dkbXp3dGN3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2NTM1ODcxNjgsImV4cCI6MTk2OTE2MzE2OH0.8FKKJqiGgoVA3p9GH5wvnbWkWywIxVLqQyZFhupZ7C4'
|
|
12
18
|
/* eslint-enable */
|
|
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
|
+
|
|
28
|
+
interface Config {
|
|
29
|
+
app: {
|
|
30
|
+
appId: string;
|
|
31
|
+
appName: string;
|
|
32
|
+
webDir: string;
|
|
33
|
+
package: {
|
|
34
|
+
version: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const getConfig = async () => {
|
|
39
|
+
let config: Config;
|
|
40
|
+
try {
|
|
41
|
+
config = await loadConfig();
|
|
42
|
+
} catch (err) {
|
|
43
|
+
program.error("No capacitor config file found, run `cap init` first");
|
|
44
|
+
}
|
|
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" })
|
|
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)
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"forceConsistentCasingInFileNames": true,
|
|
3
5
|
"module": "commonjs",
|
|
4
6
|
"esModuleInterop": true,
|
|
7
|
+
"skipLibCheck": true,
|
|
5
8
|
"resolveJsonModule": true,
|
|
6
9
|
"target": "es6",
|
|
7
10
|
"moduleResolution": "node",
|
|
@@ -11,5 +14,5 @@
|
|
|
11
14
|
"lib": ["es2016", "dom"]
|
|
12
15
|
},
|
|
13
16
|
"include": ["src/**/*"],
|
|
14
|
-
"exclude": ["node_modules", "__tests__", "**/*.spec.ts"]
|
|
17
|
+
"exclude": ["node_modules", "dist", "__tests__", "**/*.spec.ts"]
|
|
15
18
|
}
|