From cd9ae80118e64813d2400831b977779f62290c2c Mon Sep 17 00:00:00 2001 From: Mateo Morris Date: Sun, 28 Jan 2024 22:48:15 -0500 Subject: [PATCH] V2.0.0 beta.43 - icon field type, enable gitlab as depoloyment option --- package-lock.json | 14 +- package.json | 2 +- src/lib/actions.js | 69 ++++--- src/routes/[site]/+layout.svelte | 250 +++++++++++++------------ src/routes/api/deploy/+server.js | 250 +++++++++++++++++-------- src/routes/api/deploy/blobs/+server.js | 55 +++--- src/routes/api/deploy/repos/+server.js | 67 ++++--- src/routes/api/deploy/user/+server.js | 35 +++- 8 files changed, 458 insertions(+), 284 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e2654e22..5a704d778 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@fontsource/fira-code": "^5.0.5", "@iconify/svelte": "^2.2.1", - "@primocms/builder": "^0.1.58", + "@primocms/builder": "^0.1.59", "@rollup/browser": "^3.28.0", "@supabase/auth-helpers-sveltekit": "^0.10.2", "@supabase/supabase-js": "^2.31.0", @@ -1225,9 +1225,9 @@ } }, "node_modules/@primocms/builder": { - "version": "0.1.58", - "resolved": "https://registry.npmjs.org/@primocms/builder/-/builder-0.1.58.tgz", - "integrity": "sha512-LW8bN3RJh41dqDakMCuuTZjnhtzJ4ZFoJmcR1G2RNGZ2dTsci9eAJ+XNsZw+HfHPaizUvccjSlTvq7M1f7HVpA==", + "version": "0.1.59", + "resolved": "https://registry.npmjs.org/@primocms/builder/-/builder-0.1.59.tgz", + "integrity": "sha512-u5okzAE0ohnwUtPLRRtrh7HWihmrDHP6jaPhCXTd4cnHj8O3Gz9Hjs2iV/V0IR1UY1chQnigle6O6PzTaS1KWA==", "peerDependencies": { "@codemirror/autocomplete": "^6.1.0", "@codemirror/commands": "^6.0.1", @@ -7389,9 +7389,9 @@ "peer": true }, "@primocms/builder": { - "version": "0.1.58", - "resolved": "https://registry.npmjs.org/@primocms/builder/-/builder-0.1.58.tgz", - "integrity": "sha512-LW8bN3RJh41dqDakMCuuTZjnhtzJ4ZFoJmcR1G2RNGZ2dTsci9eAJ+XNsZw+HfHPaizUvccjSlTvq7M1f7HVpA==", + "version": "0.1.59", + "resolved": "https://registry.npmjs.org/@primocms/builder/-/builder-0.1.59.tgz", + "integrity": "sha512-u5okzAE0ohnwUtPLRRtrh7HWihmrDHP6jaPhCXTd4cnHj8O3Gz9Hjs2iV/V0IR1UY1chQnigle6O6PzTaS1KWA==", "requires": {} }, "@remirror/core-constants": { diff --git a/package.json b/package.json index 7243033a3..edbe77ce3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "dependencies": { "@fontsource/fira-code": "^5.0.5", "@iconify/svelte": "^2.2.1", - "@primocms/builder": "^0.1.58", + "@primocms/builder": "^0.1.59", "@rollup/browser": "^3.28.0", "@supabase/auth-helpers-sveltekit": "^0.10.2", "@supabase/supabase-js": "^2.31.0", diff --git a/src/lib/actions.js b/src/lib/actions.js index 16561de99..9f10a5dd0 100644 --- a/src/lib/actions.js +++ b/src/lib/actions.js @@ -5,26 +5,29 @@ import { invalidate } from '$app/navigation' export const sites = { create: async (data, preview = null) => { - await supabase.from('sites').insert(data.site) - // create symbols and root pages + // create symbols and root pages const { pages, symbols, sections } = data - const home_page = pages.find(page => page.url === 'index') - const root_pages = pages.filter(page => page.parent === null && page.id !== home_page.id) - const child_pages = pages.filter(page => page.parent !== null) + const home_page = pages.find((page) => page.url === 'index') + const root_pages = pages.filter( + (page) => page.parent === null && page.id !== home_page.id + ) + const child_pages = pages.filter((page) => page.parent !== null) // create home page first (to ensure it appears first) await supabase.from('pages').insert(home_page) await Promise.all([ supabase.from('symbols').insert(symbols), - supabase.from('pages').insert(root_pages) + supabase.from('pages').insert(root_pages), ]) // upload preview to supabase storage if (preview) { - await supabase.storage.from('sites').upload(`${data.site.id}/preview.html`, preview) + await supabase.storage + .from('sites') + .upload(`${data.site.id}/preview.html`, preview) } // create child pages (dependant on parent page IDs) @@ -32,36 +35,49 @@ export const sites = { // create sections (dependant on page IDs) await supabase.from('sections').insert(sections) - }, update: async (id, props) => { await supabase.from('sites').update(props).eq('id', id) }, delete: async (site, { delete_repo, delete_files }) => { - - const [{data:pages}, {data:sections}, {data:symbols}] = await Promise.all([ - supabase.from('pages').select('id, url, name, code, fields, content, site, parent').filter('site', 'eq', site.id), - supabase.from('sections').select('id, content, page!inner(id, site), symbol, index').filter('page.site', 'eq', site.id), - supabase.from('symbols').select('id, name, code, fields, content, site').filter('site', 'eq', site.id), - ]) + const [{ data: pages }, { data: sections }, { data: symbols }] = + await Promise.all([ + supabase + .from('pages') + .select('id, url, name, code, fields, content, site, parent') + .filter('site', 'eq', site.id), + supabase + .from('sections') + .select('id, content, page!inner(id, site), symbol, index') + .filter('page.site', 'eq', site.id), + supabase + .from('symbols') + .select('id, name, code, fields, content, site') + .filter('site', 'eq', site.id), + ]) // Backup site const backup_json = JSON.stringify({ site, pages, - sections: sections.map(section => ({ + sections: sections.map((section) => ({ ...section, - page: section.page.id + page: section.page.id, })), symbols, - version: 2 + version: 2, }) - await supabase.storage.from('sites').upload(`backups/${site.url}-${site.id}.json`, backup_json) - console.log({ site, pages, sections, symbols, backup_json }) + await supabase.storage + .from('sites') + .upload(`backups/${site.url}-${site.id}.json`, backup_json) if (sections) { - await Promise.all(sections.map(section => supabase.from('sections').delete().eq('id', section.id))) + await Promise.all( + sections.map((section) => + supabase.from('sections').delete().eq('id', section.id) + ) + ) } await Promise.all([ @@ -73,19 +89,22 @@ export const sites = { if (delete_files) { let siteFiles = await getFiles('sites', site.id) - if (siteFiles.length) await supabase.storage.from('sites').remove(siteFiles) + if (siteFiles.length) + await supabase.storage.from('sites').remove(siteFiles) let imageFiles = await getFiles('images', site.id) - if (imageFiles.length) await supabase.storage.from('images').remove(imageFiles) - + if (imageFiles.length) + await supabase.storage.from('images').remove(imageFiles) } if (delete_repo) { const repo_deleted = await axios.post('/api/deploy/delete', { site }) if (!repo_deleted) { - alert(`Could not delete repo. Ensure Personal Access Token has the 'delete_repo' permission`) + alert( + `Could not delete repo. Ensure Personal Access Token has the 'delete_repo' permission` + ) } } await supabase.from('sites').delete().eq('id', site.id) invalidate('app:data') }, -} \ No newline at end of file +} diff --git a/src/routes/[site]/+layout.svelte b/src/routes/[site]/+layout.svelte index 024cf42f8..5fd36af7a 100644 --- a/src/routes/[site]/+layout.svelte +++ b/src/routes/[site]/+layout.svelte @@ -1,139 +1,143 @@ - + diff --git a/src/routes/api/deploy/+server.js b/src/routes/api/deploy/+server.js index 5483281ca..88bb34eb6 100644 --- a/src/routes/api/deploy/+server.js +++ b/src/routes/api/deploy/+server.js @@ -9,7 +9,7 @@ export async function POST({ request, locals }) { throw server_error(401, { message: 'Unauthorized' }) } - const { files, site_id, repo_name, create_new, message } = + const { files, site_id, repo_name, create_new, message, provider } = await request.json() const user_id = session.user.id @@ -31,6 +31,7 @@ export async function POST({ request, locals }) { const { data: token } = await supabase_admin .from('config') .select('value') + .eq('id', `${provider}_token`) .single() if (!token) { @@ -38,14 +39,14 @@ export async function POST({ request, locals }) { } if (create_new) { - const new_deployment = await create_repo({ + const new_deployment = await git_providers[provider].create_repo({ repo_name, token: token.value, }) return json({ deployment: new_deployment, error: null }) } else { // TODO: ensure existing repo matches newer repo, or create repo if none exists and user is repo owner - const new_deployment = await push_site_to_github({ + const new_deployment = await git_providers[provider].push_site({ files, token: token.value, repo_name, @@ -64,86 +65,189 @@ export async function POST({ request, locals }) { } } -async function create_repo({ repo_name, token }) { - const repo_sans_user = repo_name.split('/')[1] - const { data } = await axios.post( - `https://api.github.com/user/repos`, - { - name: repo_sans_user, - auto_init: true, - }, - { headers: { Authorization: `Bearer ${token}` } } - ) - return data -} +const github = { + /** + * @param {CreateRepoParams} params + * @returns {Promise} + */ + create_repo: async function ({ repo_name, token }) { + const repo_sans_user = repo_name.split('/')[1] + const { data } = await axios.post( + `https://api.github.com/user/repos`, + { + name: repo_sans_user, + auto_init: true, + }, + { headers: { Authorization: `Bearer ${token}` } } + ) + return data + }, + /** + * @param {PushSiteParams} params + * @returns {Promise} + */ + push_site: async function ({ repo_name, files, message, token }) { + const headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + } -async function push_site_to_github({ files, token, repo_name, message }) { - const headers = { - Authorization: `Bearer ${token}`, - Accept: 'application/vnd.github.v3+json', - } + const [ + { data: existing_repo }, + { + data: [latest_commit], + }, + ] = await Promise.all([ + axios.get(`https://api.github.com/repos/${repo_name}`, { headers }), + axios.get(`https://api.github.com/repos/${repo_name}/commits?sha=main`, { + headers, + }), + ]) + const active_sha = latest_commit?.sha + + const tree = await create_tree() + const commit = await create_commit(tree.sha, active_sha) + const final = await push_commit(commit.sha) + + return { + deploy_id: final.object.sha, + repo: existing_repo, + created: Date.now(), + tree, + } - const [ - { data: existing_repo }, - { - data: [latest_commit], - }, - ] = await Promise.all([ - axios.get(`https://api.github.com/repos/${repo_name}`, { headers }), - axios.get(`https://api.github.com/repos/${repo_name}/commits?sha=main`, { - headers, - }), - ]) - const active_sha = latest_commit?.sha + async function create_tree() { + const tree = files.map((file) => ({ + path: file.path, + sha: file.sha, + type: 'blob', + mode: '100644', + })) + const { data } = await axios.post( + `https://api.github.com/repos/${repo_name}/git/trees`, + { tree }, + { headers } + ) + return data + } - const tree = await create_tree() - const commit = await create_commit(tree.sha, active_sha) - const final = await push_commit(commit.sha) + async function create_commit(tree, active_sha) { + const { data } = await axios.post( + `https://api.github.com/repos/${repo_name}/git/commits`, + { + message, + tree, + ...(active_sha ? { parents: [active_sha] } : {}), + }, + { headers } + ) + return data + } - return { - deploy_id: final.object.sha, - repo: existing_repo, - created: Date.now(), - tree, - } + async function push_commit(commitSha) { + const { data } = await axios.patch( + `https://api.github.com/repos/${repo_name}/git/refs/heads/main`, + { + sha: commitSha, + // force: true, + }, + { headers } + ) + return data + } + }, +} - async function create_tree() { - const tree = files.map((file) => ({ - path: file.path, - sha: file.sha, - type: 'blob', - mode: '100644', - })) - const { data } = await axios.post( - `https://api.github.com/repos/${repo_name}/git/trees`, - { tree }, - { headers } +const gitlab = { + /** + * @param {CreateRepoParams} params + * @returns {Promise} + */ + create_repo: async function ({ repo_name, token }) { + const repo_sans_user = repo_name.split('/')[1] + const response = await axios.post( + `https://gitlab.com/api/v4/projects`, + { name: repo_sans_user, initialize_with_readme: true }, + { headers: { 'PRIVATE-TOKEN': token } } ) - return data - } + console.log('Repository created:', response.data) + }, + /** + * @param {PushSiteParams} params + * @returns {Promise} + */ + push_site: async function ({ repo_name, files, message, token }) { + const project_id = encodeURIComponent(repo_name) + const headers = { 'PRIVATE-TOKEN': token } + const existing_files = await fetch_file_list(project_id) + + const actions = files.map((file) => ({ + action: existing_files.includes(file.path) ? 'update' : 'create', + file_path: file.path, + content: file.content, + })) - async function create_commit(tree, active_sha) { const { data } = await axios.post( - `https://api.github.com/repos/${repo_name}/git/commits`, - { - message, - tree, - ...(active_sha ? { parents: [active_sha] } : {}), - }, + `https://gitlab.com/api/v4/projects/${project_id}/repository/commits`, + { branch: 'main', commit_message: message, actions }, { headers } ) - return data - } - async function push_commit(commitSha) { - const { data } = await axios.patch( - `https://api.github.com/repos/${repo_name}/git/refs/heads/main`, - { - sha: commitSha, - // force: true, + console.log('Commit successful:', data) + return { + deploy_id: data.id, + repo: { + ...data, + full_name: repo_name, // copy github response for later display }, - { headers } - ) - return data - } + created: Date.now(), + } + + async function fetch_file_list(project_id) { + const fileList = await fetch_tree_files( + `https://gitlab.com/api/v4/projects/${project_id}/repository/tree` + ) + return fileList + + async function fetch_tree_files(url) { + const response = await axios.get(url, { headers }) + const items = response.data + + const filePromises = items.map(async (item) => { + if (item.type === 'blob') { + return item.path + } else if (item.type === 'tree') { + // If it's a directory (tree), recursively fetch files within it + const subUrl = `https://gitlab.com/api/v4/projects/${project_id}/repository/tree?path=${encodeURIComponent( + item.path + )}` + const subFiles = await fetch_tree_files(subUrl) + return subFiles + } + }) + + // Flatten the array of file paths and return + return (await Promise.all(filePromises)).flat() + } + } + }, +} + +const git_providers = { + github, + gitlab, } + +/** + * @typedef {Object} CreateRepoParams + * @property {string} repo_name - The name of the repository where the files will be stored. + * @property {string} token - The user's Public Access Token + */ + +/** + * @typedef {Object} PushSiteParams + * @property {string} repo_name - The name of the repository where the files will be stored. + * @property {Array} files - The files to be uploaded to the repo + * @property {string} message - The commit message + * @property {string} token - The user's Public Access Token + */ diff --git a/src/routes/api/deploy/blobs/+server.js b/src/routes/api/deploy/blobs/+server.js index e2402586d..17023b27d 100644 --- a/src/routes/api/deploy/blobs/+server.js +++ b/src/routes/api/deploy/blobs/+server.js @@ -1,4 +1,4 @@ -import { json, error as server_error } from '@sveltejs/kit'; +import { json, error as server_error } from '@sveltejs/kit' import supabase_admin from '$lib/supabase/admin' import axios from 'axios' @@ -8,30 +8,43 @@ export async function POST({ request, locals }) { // the user is not signed in throw server_error(401, { message: 'Unauthorized' }) } - - const {repo_name, files} = await request.json(); - const {data:token} = await supabase_admin.from('config').select('value').single() + const { repo_name, files, provider } = await request.json() - const res = await Promise.all(files.map(async file => { - const blob_sha = await create_blob({content: file.data, token: token.value, repo_name}) - return { - path: file.file, - sha: blob_sha - } - })) + const { data: token } = await supabase_admin + .from('config') + .select('value') + .eq('id', `${provider}_token`) + .single() + + const res = await Promise.all( + files.map(async (file) => { + const blob_sha = await create_blob({ + content: file.data, + token: token?.value, + repo_name, + }) + return { + path: file.file, + sha: blob_sha, + } + }) + ) return json(res) } -async function create_blob({content, token, repo_name}) { - const {data} = await axios.post(`https://api.github.com/repos/${repo_name}/git/blobs`, { - content: content, - encoding: 'utf-8' - }, - { - headers: { Authorization: `Bearer ${token}` } - }); +async function create_blob({ content, repo_name, token }) { + const { data } = await axios.post( + `https://api.github.com/repos/${repo_name}/git/blobs`, + { + content, + encoding: 'utf-8', + }, + { + headers: { Authorization: `Bearer ${token}` }, + } + ) - return data.sha; -} \ No newline at end of file + return data.sha +} diff --git a/src/routes/api/deploy/repos/+server.js b/src/routes/api/deploy/repos/+server.js index 7bcfb16e5..d57b5de28 100644 --- a/src/routes/api/deploy/repos/+server.js +++ b/src/routes/api/deploy/repos/+server.js @@ -1,37 +1,56 @@ -import { json, error as server_error } from '@sveltejs/kit'; +import { json, error as server_error } from '@sveltejs/kit' import supabase_admin from '$lib/supabase/admin' import axios from 'axios' -export async function GET({ locals }) { - +export async function GET({ locals, url }) { const session = await locals.getSession() if (!session) { // the user is not signed in throw server_error(401, { message: 'Unauthorized' }) } - const {data:token} = await supabase_admin.from('config').select('value').single() + const provider = url.searchParams.get('provider') - const headers = { - Authorization: `Bearer ${token.value}`, - Accept: 'application/vnd.github.v3+json' - } + const { data: token } = await supabase_admin + .from('config') + .select('value') + .eq('id', `${provider}_token`) + .single() - const res = await Promise.all([ - axios.get(`https://api.github.com/user/repos?per_page=100`, { - headers: { ...headers } - }), - axios.get(`https://api.github.com/user/repos?per_page=100&page=2`, { - headers: { ...headers } - }) - ]).then((res) => res.map(({ data }) => data)) - - const repos = res.flat().map((repo) => { - return { - id: repo.full_name, - label: repo.name + let repos = null + if (provider === 'github' && token) { + const headers = { + Authorization: `Bearer ${token.value}`, + Accept: 'application/vnd.github.v3+json', } - }) - return json(repos); -} \ No newline at end of file + const res = await Promise.all([ + axios.get(`https://api.github.com/user/repos?per_page=100`, { + headers: { ...headers }, + }), + axios.get(`https://api.github.com/user/repos?per_page=100&page=2`, { + headers: { ...headers }, + }), + ]).then((res) => res.map(({ data }) => data)) + + repos = res.flat().map((repo) => ({ + id: repo.full_name, + label: repo.name, + })) + } else if (provider === 'gitlab' && token) { + const res = await axios.get('https://gitlab.com/api/v4/projects', { + headers: { Authorization: `Bearer ${token.value}` }, + params: { + owned: true, + per_page: 100, + }, + }) + + repos = res.data?.map((project) => ({ + id: project.path_with_namespace, + label: project.name, + })) + } + + return json(repos) +} diff --git a/src/routes/api/deploy/user/+server.js b/src/routes/api/deploy/user/+server.js index 68e75351b..0959c9ae0 100644 --- a/src/routes/api/deploy/user/+server.js +++ b/src/routes/api/deploy/user/+server.js @@ -1,22 +1,37 @@ -import { json, error as server_error } from '@sveltejs/kit'; +import { json, error as server_error } from '@sveltejs/kit' import supabase_admin from '$lib/supabase/admin' import axios from 'axios' -export async function GET({ locals }) { - +export async function GET({ locals, url }) { const session = await locals.getSession() if (!session) { // the user is not signed in throw server_error(401, { message: 'Unauthorized' }) } - const {data:token, error} = await supabase_admin.from('config').select('value').single() + const provider = url.searchParams.get('provider') - const headers = { Authorization: `Bearer ${token.value}` } + const { data: token, error } = await supabase_admin + .from('config') + .select('value') + .eq('id', `${provider}_token`) + .single() - const { data } = await axios.get(`https://api.github.com/user`, { - headers: { ...headers, Accept: 'application/vnd.github.v3+json' } - }) + let data = null + if (provider === 'github' && token) { + const res = await axios.get(`https://api.github.com/user`, { + headers: { + Authorization: `Bearer ${token.value}`, + Accept: 'application/vnd.github.v3+json', + }, + }) + data = res.data + } else if (provider === 'gitlab' && token) { + const res = await axios.get(`https://gitlab.com/api/v4/user`, { + headers: { Authorization: `Bearer ${token?.value}` }, + }) + data = res.data + } - return json(data); -} \ No newline at end of file + return json(data) +}