diff options
Diffstat (limited to 'packages/db/src')
-rw-r--r-- | packages/db/src/core/cli/commands/execute/index.ts | 7 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/link/index.ts | 295 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/login/index.ts | 96 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/logout/index.ts | 7 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/push/index.ts | 40 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/shell/index.ts | 4 | ||||
-rw-r--r-- | packages/db/src/core/cli/commands/verify/index.ts | 4 | ||||
-rw-r--r-- | packages/db/src/core/cli/index.ts | 20 | ||||
-rw-r--r-- | packages/db/src/core/cli/migration-queries.ts | 38 | ||||
-rw-r--r-- | packages/db/src/core/integration/index.ts | 12 | ||||
-rw-r--r-- | packages/db/src/core/integration/vite-plugin-db.ts | 19 | ||||
-rw-r--r-- | packages/db/src/core/load-file.ts | 2 | ||||
-rw-r--r-- | packages/db/src/core/schemas.ts | 2 | ||||
-rw-r--r-- | packages/db/src/core/utils.ts | 37 | ||||
-rw-r--r-- | packages/db/src/runtime/db-client.ts | 183 |
15 files changed, 38 insertions, 728 deletions
diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts index 053736291..bf3dfd523 100644 --- a/packages/db/src/core/cli/commands/execute/index.ts +++ b/packages/db/src/core/cli/commands/execute/index.ts @@ -11,7 +11,7 @@ import { } from '../../../errors.js'; import { getLocalVirtualModContents, - getStudioVirtualModContents, + getRemoteVirtualModContents, } from '../../../integration/vite-plugin-db.js'; import { bundleFile, importBundledFile } from '../../../load-file.js'; import type { DBConfig } from '../../../types.js'; @@ -40,10 +40,9 @@ export async function cmd({ let virtualModContents: string; if (flags.remote) { - const appToken = await getManagedRemoteToken(flags.token); - virtualModContents = getStudioVirtualModContents({ + virtualModContents = getRemoteVirtualModContents({ tables: dbConfig.tables ?? {}, - appToken: appToken.token, + appToken: getManagedRemoteToken(flags.token), isBuild: false, output: 'server', }); diff --git a/packages/db/src/core/cli/commands/link/index.ts b/packages/db/src/core/cli/commands/link/index.ts deleted file mode 100644 index 4a105df9d..000000000 --- a/packages/db/src/core/cli/commands/link/index.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { homedir } from 'node:os'; -import { basename } from 'node:path'; -import { - MISSING_SESSION_ID_ERROR, - PROJECT_ID_FILE, - getAstroStudioUrl, - getSessionIdFromFile, -} from '@astrojs/studio'; -import { slug } from 'github-slugger'; -import { bgRed, cyan } from 'kleur/colors'; -import prompts from 'prompts'; -import yoctoSpinner from 'yocto-spinner'; -import { safeFetch } from '../../../../runtime/utils.js'; -import type { Result } from '../../../utils.js'; - -export async function cmd() { - const sessionToken = await getSessionIdFromFile(); - if (!sessionToken) { - console.error(MISSING_SESSION_ID_ERROR); - process.exit(1); - } - await promptBegin(); - const isLinkExisting = await promptLinkExisting(); - if (isLinkExisting) { - const workspaceId = await promptWorkspace(sessionToken); - const existingProjectData = await promptExistingProjectName({ workspaceId }); - return await linkProject(existingProjectData.id); - } - - const isLinkNew = await promptLinkNew(); - if (isLinkNew) { - const workspaceId = await promptWorkspace(sessionToken); - const newProjectName = await promptNewProjectName(); - const newProjectRegion = await promptNewProjectRegion(); - const spinner = yoctoSpinner({ text: 'Creating new project...' }).start(); - const newProjectData = await createNewProject({ - workspaceId, - name: newProjectName, - region: newProjectRegion, - }); - // TODO(fks): Actually listen for project creation before continuing - // This is just a dumb spinner that roughly matches database creation time. - await new Promise((r) => setTimeout(r, 4000)); - spinner.success('Project created!'); - return await linkProject(newProjectData.id); - } -} - -async function linkProject(id: string) { - await mkdir(new URL('.', PROJECT_ID_FILE), { recursive: true }); - await writeFile(PROJECT_ID_FILE, `${id}`); - console.info('Project linked.'); -} - -async function getWorkspaces(sessionToken: string) { - const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/workspaces.list'); - const response = await safeFetch( - linkUrl, - { - method: 'POST', - headers: { - Authorization: `Bearer ${sessionToken}`, - 'Content-Type': 'application/json', - }, - }, - (res) => { - // Unauthorized - if (res.status === 401) { - throw new Error( - `${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan( - 'astro login', - )} to authenticate and then try linking again.\n\n`, - ); - } - throw new Error(`Failed to fetch user workspace: ${res.status} ${res.statusText}`); - }, - ); - - const { data, success } = (await response.json()) as Result<{ id: string; name: string }[]>; - if (!success) { - throw new Error(`Failed to fetch user's workspace.`); - } - return data; -} - -/** - * Get the workspace ID to link to. - * Prompts the user to choose if they have more than one workspace in Astro Studio. - * @returns A `Promise` for the workspace ID to use. - */ -async function promptWorkspace(sessionToken: string) { - const workspaces = await getWorkspaces(sessionToken); - if (workspaces.length === 0) { - console.error('No workspaces found.'); - process.exit(1); - } - - if (workspaces.length === 1) { - return workspaces[0].id; - } - - const { workspaceId } = await prompts({ - type: 'autocomplete', - name: 'workspaceId', - message: 'Select your workspace:', - limit: 5, - choices: workspaces.map((w) => ({ title: w.name, value: w.id })), - }); - if (typeof workspaceId !== 'string') { - console.log('Canceled.'); - process.exit(0); - } - return workspaceId; -} - -async function createNewProject({ - workspaceId, - name, - region, -}: { - workspaceId: string; - name: string; - region: string; -}) { - const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.create'); - const response = await safeFetch( - linkUrl, - { - method: 'POST', - headers: { - Authorization: `Bearer ${await getSessionIdFromFile()}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ workspaceId, name, region }), - }, - (res) => { - // Unauthorized - if (res.status === 401) { - console.error( - `${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan( - 'astro login', - )} to authenticate and then try linking again.\n\n`, - ); - process.exit(1); - } - console.error(`Failed to create project: ${res.status} ${res.statusText}`); - process.exit(1); - }, - ); - - const { data, success } = (await response.json()) as Result<{ id: string; idName: string }>; - if (!success) { - console.error(`Failed to create project.`); - process.exit(1); - } - return { id: data.id, idName: data.idName }; -} - -async function promptExistingProjectName({ workspaceId }: { workspaceId: string }) { - const linkUrl = new URL(getAstroStudioUrl() + '/api/cli/projects.list'); - const response = await safeFetch( - linkUrl, - { - method: 'POST', - headers: { - Authorization: `Bearer ${await getSessionIdFromFile()}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ workspaceId }), - }, - (res) => { - if (res.status === 401) { - console.error( - `${bgRed('Unauthorized')}\n\n Are you logged in?\n Run ${cyan( - 'astro login', - )} to authenticate and then try linking again.\n\n`, - ); - process.exit(1); - } - console.error(`Failed to fetch projects: ${res.status} ${res.statusText}`); - process.exit(1); - }, - ); - - const { data, success } = (await response.json()) as Result< - { id: string; name: string; idName: string }[] - >; - if (!success) { - console.error(`Failed to fetch projects.`); - process.exit(1); - } - const { projectId } = await prompts({ - type: 'autocomplete', - name: 'projectId', - message: 'What is your project name?', - limit: 5, - choices: data.map((p) => ({ title: p.name, value: p.id })), - }); - if (typeof projectId !== 'string') { - console.log('Canceled.'); - process.exit(0); - } - const selectedProjectData = data.find((p: any) => p.id === projectId)!; - return selectedProjectData; -} - -async function promptBegin(): Promise<void> { - // Get the current working directory relative to the user's home directory - const prettyCwd = process.cwd().replace(homedir(), '~'); - - // prompt - const { begin } = await prompts({ - type: 'confirm', - name: 'begin', - message: `Link "${prettyCwd}" with Astro Studio?`, - initial: true, - }); - if (!begin) { - console.log('Canceled.'); - process.exit(0); - } -} - -/** - * Ask the user if they want to link to an existing Astro Studio project. - * @returns A `Promise` for the user’s answer: `true` if they answer yes, otherwise `false`. - */ -async function promptLinkExisting(): Promise<boolean> { - // prompt - const { linkExisting } = await prompts({ - type: 'confirm', - name: 'linkExisting', - message: `Link with an existing project in Astro Studio?`, - initial: true, - }); - return !!linkExisting; -} - -/** - * Ask the user if they want to link to a new Astro Studio Project. - * **Exits the process if they answer no.** - * @returns A `Promise` for the user’s answer: `true` if they answer yes. - */ -async function promptLinkNew(): Promise<boolean> { - // prompt - const { linkNew } = await prompts({ - type: 'confirm', - name: 'linkNew', - message: `Create a new project in Astro Studio?`, - initial: true, - }); - if (!linkNew) { - console.log('Canceled.'); - process.exit(0); - } - return true; -} - -async function promptNewProjectName(): Promise<string> { - const { newProjectName } = await prompts({ - type: 'text', - name: 'newProjectName', - message: `What is your new project's name?`, - initial: basename(process.cwd()), - format: (val) => slug(val), - }); - if (!newProjectName) { - console.log('Canceled.'); - process.exit(0); - } - return newProjectName; -} - -async function promptNewProjectRegion(): Promise<string> { - const { newProjectRegion } = await prompts({ - type: 'select', - name: 'newProjectRegion', - message: `Where should your new database live?`, - choices: [ - { title: 'North America (East)', value: 'NorthAmericaEast' }, - { title: 'North America (West)', value: 'NorthAmericaWest' }, - { title: 'Europe (Amsterdam)', value: 'EuropeCentral' }, - { title: 'South America (Brazil)', value: 'SouthAmericaEast' }, - { title: 'Asia (India)', value: 'AsiaSouth' }, - { title: 'Asia (Japan)', value: 'AsiaNorthEast' }, - ], - initial: 0, - }); - if (!newProjectRegion) { - console.log('Canceled.'); - process.exit(0); - } - return newProjectRegion; -} diff --git a/packages/db/src/core/cli/commands/login/index.ts b/packages/db/src/core/cli/commands/login/index.ts deleted file mode 100644 index 0b0979384..000000000 --- a/packages/db/src/core/cli/commands/login/index.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { mkdir, writeFile } from 'node:fs/promises'; -import { createServer as _createServer } from 'node:http'; -import { SESSION_LOGIN_FILE, getAstroStudioUrl } from '@astrojs/studio'; -import type { AstroConfig } from 'astro'; -import { listen } from 'async-listen'; -import { cyan } from 'kleur/colors'; -import open from 'open'; -import prompt from 'prompts'; -import type { Arguments } from 'yargs-parser'; -import yoctoSpinner from 'yocto-spinner'; -import type { DBConfig } from '../../../types.js'; - -const isWebContainer = - // Stackblitz heuristic - process.versions?.webcontainer ?? - // GitHub Codespaces heuristic - process.env.CODESPACE_NAME; - -export async function cmd({ - flags, -}: { - astroConfig: AstroConfig; - dbConfig: DBConfig; - flags: Arguments; -}) { - let session = flags.session; - - if (!session && isWebContainer) { - console.log(`Please visit the following URL in your web browser:`); - console.log(cyan(`${getAstroStudioUrl()}/auth/cli/login`)); - console.log(`After login in complete, enter the verification code displayed:`); - const response = await prompt({ - type: 'text', - name: 'session', - message: 'Verification code:', - }); - if (!response.session) { - console.error('Cancelling login.'); - process.exit(0); - } - session = response.session; - console.log('Successfully logged in'); - } else if (!session) { - const { url, promise } = await createServer(); - const loginUrl = new URL('/auth/cli/login', getAstroStudioUrl()); - loginUrl.searchParams.set('returnTo', url); - console.log(`Opening the following URL in your browser...`); - console.log(cyan(loginUrl.href)); - console.log(`If something goes wrong, copy-and-paste the URL into your browser.`); - open(loginUrl.href); - const spinner = yoctoSpinner({ text: 'Waiting for confirmation...' }); - session = await promise; - spinner.success('Successfully logged in'); - } - - await mkdir(new URL('.', SESSION_LOGIN_FILE), { recursive: true }); - await writeFile(SESSION_LOGIN_FILE, `${session}`); -} - -// NOTE(fks): How the Astro CLI login process works: -// 1. The Astro CLI creates a temporary server to listen for the session token -// 2. The user is directed to studio.astro.build/ to login -// 3. The user is redirected back to the temporary server with their session token -// 4. The temporary server receives and saves the session token, logging the user in -// 5. The user is redirected one last time to a success/failure page -async function createServer(): Promise<{ url: string; promise: Promise<string> }> { - let resolve: (value: string | PromiseLike<string>) => void, reject: (reason?: Error) => void; - - const server = _createServer((req, res) => { - // Handle the request - const url = new URL(req.url ?? '/', `http://${req.headers.host}`); - const sessionParam = url.searchParams.get('session'); - // Handle the response & resolve the promise - res.statusCode = 302; - if (!sessionParam) { - res.setHeader('location', getAstroStudioUrl() + '/auth/cli/error'); - reject(new Error('Failed to log in')); - } else { - res.setHeader('location', getAstroStudioUrl() + '/auth/cli/success'); - resolve(sessionParam); - } - res.end(); - }); - - const { port } = await listen(server, 0, '127.0.0.1'); - const serverUrl = `http://localhost:${port}`; - const sessionPromise = new Promise<string>((_resolve, _reject) => { - resolve = _resolve; - reject = _reject; - }).finally(() => { - server.closeAllConnections(); - server.close(); - }); - - return { url: serverUrl, promise: sessionPromise }; -} diff --git a/packages/db/src/core/cli/commands/logout/index.ts b/packages/db/src/core/cli/commands/logout/index.ts deleted file mode 100644 index 8b7878659..000000000 --- a/packages/db/src/core/cli/commands/logout/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { unlink } from 'node:fs/promises'; -import { SESSION_LOGIN_FILE } from '@astrojs/studio'; - -export async function cmd() { - await unlink(SESSION_LOGIN_FILE); - console.log('Successfully logged out of Astro Studio.'); -} diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index 590d4f06e..46a457884 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -3,12 +3,10 @@ import { sql } from 'drizzle-orm'; import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; import { createRemoteDatabaseClient } from '../../../../runtime/index.js'; -import { safeFetch } from '../../../../runtime/utils.js'; import { MIGRATION_VERSION } from '../../../consts.js'; import type { DBConfig, DBSnapshot } from '../../../types.js'; import { type RemoteDatabaseInfo, - type Result, getManagedRemoteToken, getRemoteDatabaseInfo, } from '../../../utils.js'; @@ -31,10 +29,10 @@ export async function cmd({ const isDryRun = flags.dryRun; const isForceReset = flags.forceReset; const dbInfo = getRemoteDatabaseInfo(); - const appToken = await getManagedRemoteToken(flags.token, dbInfo); + const appToken = getManagedRemoteToken(flags.token); const productionSnapshot = await getProductionCurrentSnapshot({ dbInfo, - appToken: appToken.token, + appToken: appToken, }); const currentSnapshot = createCurrentSnapshot(dbConfig); const isFromScratch = !productionSnapshot; @@ -77,13 +75,11 @@ export async function cmd({ await pushSchema({ statements: migrationQueries, dbInfo, - appToken: appToken.token, + appToken: appToken, isDryRun, currentSnapshot: currentSnapshot, }); } - // cleanup and exit - await appToken.destroy(); console.info('Push complete!'); } @@ -110,9 +106,7 @@ async function pushSchema({ return new Response(null, { status: 200 }); } - return dbInfo.type === 'studio' - ? pushToStudio(requestBody, appToken, dbInfo.url) - : pushToDb(requestBody, appToken, dbInfo.url); + return pushToDb(requestBody, appToken, dbInfo.url); } type RequestBody = { @@ -145,29 +139,3 @@ async function pushToDb(requestBody: RequestBody, appToken: string, remoteUrl: s )`); }); } - -async function pushToStudio(requestBody: RequestBody, appToken: string, remoteUrl: string) { - const url = new URL('/db/push', remoteUrl); - const response = await safeFetch( - url, - { - method: 'POST', - headers: new Headers({ - Authorization: `Bearer ${appToken}`, - }), - body: JSON.stringify(requestBody), - }, - async (res) => { - console.error(`${url.toString()} failed: ${res.status} ${res.statusText}`); - console.error(await res.text()); - throw new Error(`/db/push fetch failed: ${res.status} ${res.statusText}`); - }, - ); - - const result = (await response.json()) as Result<never>; - if (!result.success) { - console.error(`${url.toString()} unsuccessful`); - console.error(await response.text()); - throw new Error(`/db/push fetch unsuccessful`); - } -} diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts index dcc54fc70..278830b4e 100644 --- a/packages/db/src/core/cli/commands/shell/index.ts +++ b/packages/db/src/core/cli/commands/shell/index.ts @@ -26,14 +26,12 @@ export async function cmd({ } const dbInfo = getRemoteDatabaseInfo(); if (flags.remote) { - const appToken = await getManagedRemoteToken(flags.token, dbInfo); const db = createRemoteDatabaseClient({ dbType: dbInfo.type, remoteUrl: dbInfo.url, - appToken: appToken.token, + appToken: getManagedRemoteToken(flags.token), }); const result = await db.run(sql.raw(query)); - await appToken.destroy(); console.log(result); } else { const { ASTRO_DATABASE_FILE } = getAstroEnv(); diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts index 35f489a80..6499c272e 100644 --- a/packages/db/src/core/cli/commands/verify/index.ts +++ b/packages/db/src/core/cli/commands/verify/index.ts @@ -20,10 +20,9 @@ export async function cmd({ }) { const isJson = flags.json; const dbInfo = getRemoteDatabaseInfo(); - const appToken = await getManagedRemoteToken(flags.token, dbInfo); const productionSnapshot = await getProductionCurrentSnapshot({ dbInfo, - appToken: appToken.token, + appToken: getManagedRemoteToken(flags.token), }); const currentSnapshot = createCurrentSnapshot(dbConfig); const { queries: migrationQueries, confirmations } = await getMigrationQueries({ @@ -53,6 +52,5 @@ export async function cmd({ console.log(result.message); } - await appToken.destroy(); process.exit(result.exitCode); } diff --git a/packages/db/src/core/cli/index.ts b/packages/db/src/core/cli/index.ts index 531b016a6..6c54c2ae6 100644 --- a/packages/db/src/core/cli/index.ts +++ b/packages/db/src/core/cli/index.ts @@ -41,18 +41,6 @@ export async function cli({ const { cmd } = await import('./commands/execute/index.js'); return await cmd({ astroConfig, dbConfig, flags }); } - case 'login': { - const { cmd } = await import('./commands/login/index.js'); - return await cmd({ astroConfig, dbConfig, flags }); - } - case 'logout': { - const { cmd } = await import('./commands/logout/index.js'); - return await cmd(); - } - case 'link': { - const { cmd } = await import('./commands/link/index.js'); - return await cmd(); - } default: { if (command != null) { console.error(`Unknown command: ${command}`); @@ -63,15 +51,15 @@ export async function cli({ headline: ' ', tables: { Commands: [ - ['push', 'Push table schema updates to Astro Studio.'], - ['verify', 'Test schema updates /w Astro Studio (good for CI).'], + ['push', 'Push table schema updates to libSQL.'], + ['verify', 'Test schema updates /w libSQL (good for CI).'], [ 'astro db execute <file-path>', - 'Execute a ts/js file using astro:db. Use --remote to connect to Studio.', + 'Execute a ts/js file using astro:db. Use --remote to connect to libSQL.', ], [ 'astro db shell --query <sql-string>', - 'Execute a SQL string. Use --remote to connect to Studio.', + 'Execute a SQL string. Use --remote to connect to libSQL.', ], ], }, diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index db3972d09..0f369e684 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -7,7 +7,7 @@ import { customAlphabet } from 'nanoid'; import { hasPrimaryKey } from '../../runtime/index.js'; import { createRemoteDatabaseClient } from '../../runtime/index.js'; import { isSerializedSQL } from '../../runtime/types.js'; -import { isDbError, safeFetch } from '../../runtime/utils.js'; +import { isDbError } from '../../runtime/utils.js'; import { MIGRATION_VERSION } from '../consts.js'; import { RENAME_COLUMN_ERROR, RENAME_TABLE_ERROR } from '../errors.js'; import { @@ -35,7 +35,7 @@ import type { ResolvedIndexes, TextColumn, } from '../types.js'; -import type { RemoteDatabaseInfo, Result } from '../utils.js'; +import type { RemoteDatabaseInfo } from '../utils.js'; const sqlite = new SQLiteAsyncDialect(); const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); @@ -428,9 +428,7 @@ export function getProductionCurrentSnapshot(options: { dbInfo: RemoteDatabaseInfo; appToken: string; }): Promise<DBSnapshot | undefined> { - return options.dbInfo.type === 'studio' - ? getStudioCurrentSnapshot(options.appToken, options.dbInfo.url) - : getDbCurrentSnapshot(options.appToken, options.dbInfo.url); + return getDbCurrentSnapshot(options.appToken, options.dbInfo.url); } async function getDbCurrentSnapshot( @@ -473,36 +471,6 @@ async function getDbCurrentSnapshot( } } -async function getStudioCurrentSnapshot( - appToken: string, - remoteUrl: string, -): Promise<DBSnapshot | undefined> { - const url = new URL('/db/schema', remoteUrl); - - const response = await safeFetch( - url, - { - method: 'POST', - headers: new Headers({ - Authorization: `Bearer ${appToken}`, - }), - }, - async (res) => { - console.error(`${url.toString()} failed: ${res.status} ${res.statusText}`); - console.error(await res.text()); - throw new Error(`/db/schema fetch failed: ${res.status} ${res.statusText}`); - }, - ); - - const result = (await response.json()) as Result<DBSnapshot>; - if (!result.success) { - console.error(`${url.toString()} unsuccessful`); - console.error(await response.text()); - throw new Error(`/db/schema fetch unsuccessful`); - } - return result.data; -} - function getDropTableQueriesForSnapshot(snapshot: DBSnapshot) { const queries = []; for (const tableName of Object.keys(snapshot.schema)) { diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts index c6f58d2fd..bc038c7d2 100644 --- a/packages/db/src/core/integration/index.ts +++ b/packages/db/src/core/integration/index.ts @@ -2,7 +2,6 @@ import { existsSync } from 'node:fs'; import { mkdir, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; -import type { ManagedAppToken } from '@astrojs/studio'; import type { AstroIntegration } from 'astro'; import { blue, yellow } from 'kleur/colors'; import { @@ -33,7 +32,6 @@ function astroDBIntegration(): AstroIntegration { let connectToRemote = false; let configFileDependencies: string[] = []; let root: URL; - let appToken: ManagedAppToken | undefined; // Used during production builds to load seed files. let tempViteServer: ViteDevServer | undefined; @@ -72,10 +70,9 @@ function astroDBIntegration(): AstroIntegration { connectToRemote = process.env.ASTRO_INTERNAL_TEST_REMOTE || args['remote']; if (connectToRemote) { - appToken = await getManagedRemoteToken(); dbPlugin = vitePluginDb({ - connectToStudio: connectToRemote, - appToken: appToken.token, + connectToRemote: connectToRemote, + appToken: getManagedRemoteToken(), tables, root: config.root, srcDir: config.srcDir, @@ -84,7 +81,7 @@ function astroDBIntegration(): AstroIntegration { }); } else { dbPlugin = vitePluginDb({ - connectToStudio: false, + connectToRemote: false, tables, seedFiles, root: config.root, @@ -161,7 +158,7 @@ function astroDBIntegration(): AstroIntegration { if (!connectToRemote && !databaseFileEnvDefined() && finalBuildOutput === 'server') { const message = `Attempting to build without the --remote flag or the ASTRO_DATABASE_FILE environment variable defined. You probably want to pass --remote to astro build.`; const hint = - 'Learn more connecting to Studio: https://docs.astro.build/en/guides/astro-db/#connect-to-astro-studio'; + 'Learn more connecting to libSQL: https://docs.astro.build/en/guides/astro-db/#connect-a-libsql-database-for-production'; throw new AstroDbError(message, hint); } @@ -174,7 +171,6 @@ function astroDBIntegration(): AstroIntegration { }; }, 'astro:build:done': async ({}) => { - await appToken?.destroy(); await tempViteServer?.close(); }, }, diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts index 410d49157..7a7173bfc 100644 --- a/packages/db/src/core/integration/vite-plugin-db.ts +++ b/packages/db/src/core/integration/vite-plugin-db.ts @@ -34,7 +34,7 @@ export type SeedHandler = { type VitePluginDBParams = | { - connectToStudio: false; + connectToRemote: false; tables: LateTables; seedFiles: LateSeedFiles; srcDir: URL; @@ -44,7 +44,7 @@ type VitePluginDBParams = seedHandler: SeedHandler; } | { - connectToStudio: true; + connectToRemote: true; tables: LateTables; appToken: string; srcDir: URL; @@ -71,8 +71,8 @@ export function vitePluginDb(params: VitePluginDBParams): VitePlugin { async load(id) { if (id !== resolved.module && id !== resolved.importedFromSeedFile) return; - if (params.connectToStudio) { - return getStudioVirtualModContents({ + if (params.connectToRemote) { + return getRemoteVirtualModContents({ appToken: params.appToken, tables: params.tables.get(), isBuild: command === 'build', @@ -138,7 +138,7 @@ export * from ${RUNTIME_VIRTUAL_IMPORT}; ${getStringifiedTableExports(tables)}`; } -export function getStudioVirtualModContents({ +export function getRemoteVirtualModContents({ tables, appToken, isBuild, @@ -153,13 +153,12 @@ export function getStudioVirtualModContents({ function appTokenArg() { if (isBuild) { - const envPrefix = dbInfo.type === 'studio' ? 'ASTRO_STUDIO' : 'ASTRO_DB'; if (output === 'server') { // In production build, always read the runtime environment variable. - return `process.env.${envPrefix}_APP_TOKEN`; + return `process.env.ASTRO_DB_APP_TOKEN`; } else { // Static mode or prerendering needs the local app token. - return `process.env.${envPrefix}_APP_TOKEN ?? ${JSON.stringify(appToken)}`; + return `process.env.ASTRO_DB_APP_TOKEN ?? ${JSON.stringify(appToken)}`; } } else { return JSON.stringify(appToken); @@ -171,9 +170,7 @@ export function getStudioVirtualModContents({ if (isBuild) { // Allow overriding, mostly for testing - return dbInfo.type === 'studio' - ? `import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL ?? ${dbStr}` - : `import.meta.env.ASTRO_DB_REMOTE_URL ?? ${dbStr}`; + return `import.meta.env.ASTRO_DB_REMOTE_URL ?? ${dbStr}`; } else { return dbStr; } diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts index 027deaa60..f7a4226b6 100644 --- a/packages/db/src/core/load-file.ts +++ b/packages/db/src/core/load-file.ts @@ -144,8 +144,8 @@ export async function bundleFile({ format: 'esm', sourcemap: 'inline', metafile: true, + // TODO: use astro:env define: { - 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined', 'import.meta.env.ASTRO_DB_REMOTE_DB_URL': 'undefined', 'import.meta.env.ASTRO_DATABASE_FILE': JSON.stringify(ASTRO_DATABASE_FILE ?? ''), }, diff --git a/packages/db/src/core/schemas.ts b/packages/db/src/core/schemas.ts index c9575a79a..1147538ce 100644 --- a/packages/db/src/core/schemas.ts +++ b/packages/db/src/core/schemas.ts @@ -94,7 +94,7 @@ const textColumnBaseSchema = baseColumnSchema z.object({ // text primary key allows NULL values. // NULL values bypass unique checks, which could - // lead to duplicate URLs per record in Astro Studio. + // lead to duplicate URLs per record. // disable `optional` for primary keys. primaryKey: z.literal(true), optional: z.literal(false).optional(), diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts index b246997e2..3348e90a0 100644 --- a/packages/db/src/core/utils.ts +++ b/packages/db/src/core/utils.ts @@ -1,4 +1,3 @@ -import { type ManagedAppToken, getAstroStudioEnv, getManagedAppTokenOrExit } from '@astrojs/studio'; import type { AstroConfig, AstroIntegration } from 'astro'; import { loadEnv } from 'vite'; import './types.js'; @@ -11,49 +10,23 @@ export function getAstroEnv(envMode = ''): Record<`ASTRO_${string}`, string> { } export type RemoteDatabaseInfo = { - type: 'libsql' | 'studio'; + type: 'libsql'; url: string; }; export function getRemoteDatabaseInfo(): RemoteDatabaseInfo { const astroEnv = getAstroEnv(); - const studioEnv = getAstroStudioEnv(); - - if (studioEnv.ASTRO_STUDIO_REMOTE_DB_URL) - return { - type: 'studio', - url: studioEnv.ASTRO_STUDIO_REMOTE_DB_URL, - }; - - if (astroEnv.ASTRO_DB_REMOTE_URL) - return { - type: 'libsql', - url: astroEnv.ASTRO_DB_REMOTE_URL, - }; return { - type: 'studio', - url: 'https://db.services.astro.build', + type: 'libsql', + url: astroEnv.ASTRO_DB_REMOTE_URL, }; } -export function getManagedRemoteToken( - token?: string, - dbInfo?: RemoteDatabaseInfo, -): Promise<ManagedAppToken> { - dbInfo ??= getRemoteDatabaseInfo(); - - if (dbInfo.type === 'studio') { - return getManagedAppTokenOrExit(token); - } - +export function getManagedRemoteToken(token?: string): string { const astroEnv = getAstroEnv(); - return Promise.resolve({ - token: token ?? astroEnv.ASTRO_DB_APP_TOKEN, - renew: () => Promise.resolve(), - destroy: () => Promise.resolve(), - }); + return token ?? astroEnv.ASTRO_DB_APP_TOKEN; } export function getDbDirectoryUrl(root: URL | string) { diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts index 55288951d..e45a2d717 100644 --- a/packages/db/src/runtime/db-client.ts +++ b/packages/db/src/runtime/db-client.ts @@ -1,10 +1,7 @@ -import type { InStatement } from '@libsql/client'; import { type Config as LibSQLConfig, createClient } from '@libsql/client'; import type { LibSQLDatabase } from 'drizzle-orm/libsql'; import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql'; -import { type SqliteRemoteDatabase, drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy'; -import { z } from 'zod'; -import { DetailedLibsqlError, safeFetch } from './utils.js'; +import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; const isWebContainer = !!process.versions?.webcontainer; @@ -34,16 +31,8 @@ export function createLocalDatabaseClient(options: LocalDbClientOptions): LibSQL return db; } -const remoteResultSchema = z.object({ - columns: z.array(z.string()), - columnTypes: z.array(z.string()), - rows: z.array(z.array(z.unknown())), - rowsAffected: z.number(), - lastInsertRowid: z.unknown().optional(), -}); - type RemoteDbClientOptions = { - dbType: 'studio' | 'libsql'; + dbType: 'libsql'; appToken: string; remoteUrl: string | URL; }; @@ -51,9 +40,7 @@ type RemoteDbClientOptions = { export function createRemoteDatabaseClient(options: RemoteDbClientOptions) { const remoteUrl = new URL(options.remoteUrl); - return options.dbType === 'studio' - ? createStudioDatabaseClient(options.appToken, remoteUrl) - : createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString()); + return createRemoteLibSQLClient(options.appToken, remoteUrl, options.remoteUrl.toString()); } // this function parses the options from a `Record<string, string>` @@ -99,167 +86,3 @@ function createRemoteLibSQLClient(appToken: string, remoteDbURL: URL, rawUrl: st const client = createClient({ ...parseOpts(options), url, authToken: appToken }); return drizzleLibsql(client); } - -function createStudioDatabaseClient(appToken: string, remoteDbURL: URL) { - if (appToken == null) { - throw new Error(`Cannot create a remote client: missing app token.`); - } - - const url = new URL('/db/query', remoteDbURL); - - const db = drizzleProxy( - async (sql, parameters, method) => { - const requestBody: InStatement = { sql, args: parameters }; - const res = await safeFetch( - url, - { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }, - async (response) => { - throw await parseRemoteError(response); - }, - ); - - let remoteResult: z.infer<typeof remoteResultSchema>; - try { - const json = await res.json(); - remoteResult = remoteResultSchema.parse(json); - } catch { - throw new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(res), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - - if (method === 'run') { - const rawRows = Array.from(remoteResult.rows); - // Implement basic `toJSON()` for Drizzle to serialize properly - (remoteResult as any).rows.toJSON = () => rawRows; - // Using `db.run()` drizzle massages the rows into an object. - // So in order to make dev/prod consistent, we need to do the same here. - // This creates an object and loops over each row replacing it with the object. - for (let i = 0; i < remoteResult.rows.length; i++) { - let row = remoteResult.rows[i]; - let item: Record<string, any> = {}; - remoteResult.columns.forEach((col, index) => { - item[col] = row[index]; - }); - (remoteResult as any).rows[i] = item; - } - return remoteResult; - } - - // Drizzle expects each row as an array of its values - const rowValues: unknown[][] = []; - - for (const row of remoteResult.rows) { - if (row != null && typeof row === 'object') { - rowValues.push(Object.values(row)); - } - } - - if (method === 'get') { - return { rows: rowValues[0] }; - } - - return { rows: rowValues }; - }, - async (queries) => { - const stmts: InStatement[] = queries.map(({ sql, params }) => ({ sql, args: params })); - const res = await safeFetch( - url, - { - method: 'POST', - headers: { - Authorization: `Bearer ${appToken}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify(stmts), - }, - async (response) => { - throw await parseRemoteError(response); - }, - ); - - let remoteResults: z.infer<typeof remoteResultSchema>[]; - try { - const json = await res.json(); - remoteResults = z.array(remoteResultSchema).parse(json); - } catch { - throw new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(res), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - let results: any[] = []; - for (const [idx, rawResult] of remoteResults.entries()) { - if (queries[idx]?.method === 'run') { - results.push(rawResult); - continue; - } - - // Drizzle expects each row as an array of its values - const rowValues: unknown[][] = []; - - for (const row of rawResult.rows) { - if (row != null && typeof row === 'object') { - rowValues.push(Object.values(row)); - } - } - - if (queries[idx]?.method === 'get') { - results.push({ rows: rowValues[0] }); - } - - results.push({ rows: rowValues }); - } - return results; - }, - ); - applyTransactionNotSupported(db); - return db; -} - -const errorSchema = z.object({ - success: z.boolean(), - error: z.object({ - code: z.string(), - details: z.string().optional(), - }), -}); - -const KNOWN_ERROR_CODES = { - SQL_QUERY_FAILED: 'SQL_QUERY_FAILED', -}; - -const getUnexpectedResponseMessage = async (response: Response) => - `Unexpected response from remote database:\n(Status ${response.status}) ${await response - .clone() - .text()}`; - -async function parseRemoteError(response: Response): Promise<DetailedLibsqlError> { - let error; - try { - error = errorSchema.parse(await response.clone().json()).error; - } catch { - return new DetailedLibsqlError({ - message: await getUnexpectedResponseMessage(response), - code: KNOWN_ERROR_CODES.SQL_QUERY_FAILED, - }); - } - // Strip LibSQL error prefixes - let baseDetails = - error.details?.replace(/.*SQLite error: /, '') ?? 'Error querying remote database.'; - // Remove duplicated "code" in details - const details = baseDetails.slice(baseDetails.indexOf(':') + 1).trim(); - let hint = `See the Astro DB guide for query and push instructions: https://docs.astro.build/en/guides/astro-db/#query-your-database`; - if (error.code === KNOWN_ERROR_CODES.SQL_QUERY_FAILED && details.includes('no such table')) { - hint = `Did you run \`astro db push\` to push your latest table schemas?`; - } - return new DetailedLibsqlError({ message: details, code: error.code, hint }); -} |