@astrojs/db 0.9.8 → 0.9.10

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.
@@ -48,7 +48,7 @@ async function cmd({
48
48
  const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl });
49
49
  const mod = await importBundledFile({ code, root: astroConfig.root });
50
50
  if (typeof mod.default !== "function") {
51
- console.error(EXEC_DEFAULT_EXPORT_ERROR);
51
+ console.error(EXEC_DEFAULT_EXPORT_ERROR(filePath));
52
52
  process.exit(1);
53
53
  }
54
54
  try {
@@ -27,7 +27,7 @@ function printHelp({
27
27
  message.push(
28
28
  linebreak(),
29
29
  ` ${bgGreen(black(` ${commandName} `))} ${green(
30
- `v${"0.9.8"}`
30
+ `v${"0.9.10"}`
31
31
  )} ${headline}`
32
32
  );
33
33
  }
@@ -1,18 +1,19 @@
1
1
  import { existsSync } from "fs";
2
2
  import { dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { AstroError } from "astro/errors";
5
4
  import { mkdir, writeFile } from "fs/promises";
6
5
  import { blue, yellow } from "kleur/colors";
7
6
  import { loadEnv } from "vite";
8
7
  import parseArgs from "yargs-parser";
8
+ import { SEED_DEV_FILE_NAME } from "../../runtime/queries.js";
9
+ import { AstroDbError } from "../../runtime/utils.js";
9
10
  import { CONFIG_FILE_NAMES, DB_PATH } from "../consts.js";
10
11
  import { resolveDbConfig } from "../load-file.js";
11
12
  import { getManagedAppTokenOrExit } from "../tokens.js";
12
13
  import { getDbDirectoryUrl } from "../utils.js";
13
14
  import { fileURLIntegration } from "./file-url.js";
14
15
  import { typegenInternal } from "./typegen.js";
15
- import { vitePluginDb } from "./vite-plugin-db.js";
16
+ import { resolved, vitePluginDb } from "./vite-plugin-db.js";
16
17
  import { vitePluginInjectEnvTs } from "./vite-plugin-inject-env-ts.js";
17
18
  function astroDBIntegration() {
18
19
  let connectToStudio = false;
@@ -82,14 +83,7 @@ function astroDBIntegration() {
82
83
  }
83
84
  await typegenInternal({ tables: tables.get() ?? {}, root: config.root });
84
85
  },
85
- "astro:server:start": async ({ logger }) => {
86
- setTimeout(() => {
87
- logger.info(
88
- connectToStudio ? "Connected to remote database." : "New local database created."
89
- );
90
- }, 100);
91
- },
92
- "astro:server:setup": async ({ server }) => {
86
+ "astro:server:setup": async ({ server, logger }) => {
93
87
  const filesToWatch = [
94
88
  ...CONFIG_FILE_NAMES.map((c) => new URL(c, getDbDirectoryUrl(root))),
95
89
  ...configFileDependencies.map((c) => new URL(c, root))
@@ -100,12 +94,51 @@ function astroDBIntegration() {
100
94
  server.restart();
101
95
  }
102
96
  });
97
+ setTimeout(() => {
98
+ logger.info(
99
+ connectToStudio ? "Connected to remote database." : "New local database created."
100
+ );
101
+ if (connectToStudio)
102
+ return;
103
+ const localSeedPaths = SEED_DEV_FILE_NAME.map(
104
+ (name) => new URL(name, getDbDirectoryUrl(root))
105
+ );
106
+ let seedInFlight = false;
107
+ if (seedFiles.get().length || localSeedPaths.find((path) => existsSync(path))) {
108
+ loadSeedModule();
109
+ }
110
+ const eagerReloadIntegrationSeedPaths = seedFiles.get().map((s) => typeof s === "string" && s.startsWith(".") ? new URL(s, root) : s).filter((s) => s instanceof URL);
111
+ const eagerReloadSeedPaths = [...eagerReloadIntegrationSeedPaths, ...localSeedPaths];
112
+ server.watcher.on("all", (event, relativeEntry) => {
113
+ if (event === "unlink" || event === "unlinkDir")
114
+ return;
115
+ const entry = new URL(relativeEntry, root);
116
+ if (eagerReloadSeedPaths.find((path) => entry.href === path.href)) {
117
+ loadSeedModule();
118
+ }
119
+ });
120
+ function loadSeedModule() {
121
+ if (seedInFlight)
122
+ return;
123
+ seedInFlight = true;
124
+ const mod = server.moduleGraph.getModuleById(resolved.seedVirtual);
125
+ if (mod)
126
+ server.moduleGraph.invalidateModule(mod);
127
+ server.ssrLoadModule(resolved.seedVirtual).then(() => {
128
+ logger.info("Seeded database.");
129
+ }).catch((e) => {
130
+ logger.error(e instanceof Error ? e.message : String(e));
131
+ }).finally(() => {
132
+ seedInFlight = false;
133
+ });
134
+ }
135
+ }, 100);
103
136
  },
104
137
  "astro:build:start": async ({ logger }) => {
105
138
  if (!connectToStudio && !databaseFileEnvDefined() && (output === "server" || output === "hybrid")) {
106
139
  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.`;
107
140
  const hint = "Learn more connecting to Studio: https://docs.astro.build/en/guides/astro-db/#connect-to-astro-studio";
108
- throw new AstroError(message, hint);
141
+ throw new AstroDbError(message, hint);
109
142
  }
110
143
  logger.info("database: " + (connectToStudio ? yellow("remote") : blue("local database.")));
111
144
  },
@@ -1,5 +1,9 @@
1
1
  import type { DBTables } from '../types.js';
2
2
  import { type VitePlugin } from '../utils.js';
3
+ export declare const resolved: {
4
+ virtual: string;
5
+ seedVirtual: string;
6
+ };
3
7
  export type LateTables = {
4
8
  get: () => DBTables;
5
9
  };
@@ -74,12 +74,11 @@ function getLocalVirtualModContents({
74
74
  });
75
75
  const dbUrl = new URL(DB_PATH, root);
76
76
  return `
77
- import { asDrizzleTable, createLocalDatabaseClient } from ${RUNTIME_IMPORT};
77
+ import { asDrizzleTable, createLocalDatabaseClient, normalizeDatabaseUrl } from ${RUNTIME_IMPORT};
78
78
  ${shouldSeed ? `import { seedLocal } from ${RUNTIME_IMPORT};` : ""}
79
79
  ${shouldSeed ? integrationSeedImportStatements.join("\n") : ""}
80
80
 
81
- const dbUrl = import.meta.env.ASTRO_DATABASE_FILE ?? ${JSON.stringify(dbUrl)};
82
-
81
+ const dbUrl = normalizeDatabaseUrl(import.meta.env.ASTRO_DATABASE_FILE, ${JSON.stringify(dbUrl)});
83
82
  export const db = createLocalDatabaseClient({ dbUrl });
84
83
 
85
84
  ${shouldSeed ? `await seedLocal({
@@ -130,5 +129,6 @@ export {
130
129
  getConfigVirtualModContents,
131
130
  getLocalVirtualModContents,
132
131
  getStudioVirtualModContents,
132
+ resolved,
133
133
  vitePluginDb
134
134
  };
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { bold, cyan } from "kleur/colors";
5
+ import { bold } from "kleur/colors";
6
6
  import { normalizePath } from "vite";
7
7
  import { DB_TYPES_FILE } from "../consts.js";
8
8
  function vitePluginInjectEnvTs({ srcDir, root }, logger) {
@@ -2,7 +2,7 @@ import { createClient } from "@libsql/client";
2
2
  import { drizzle as drizzleLibsql } from "drizzle-orm/libsql";
3
3
  import { drizzle as drizzleProxy } from "drizzle-orm/sqlite-proxy";
4
4
  import { z } from "zod";
5
- import { safeFetch } from "./utils.js";
5
+ import { AstroDbError, safeFetch } from "./utils.js";
6
6
  const isWebContainer = !!process.versions?.webcontainer;
7
7
  function applyTransactionNotSupported(db) {
8
8
  Object.assign(db, {
@@ -45,12 +45,8 @@ function createRemoteDatabaseClient(appToken, remoteDbURL) {
45
45
  },
46
46
  body: JSON.stringify(requestBody)
47
47
  },
48
- (response) => {
49
- throw new Error(
50
- `Failed to execute query.
51
- Query: ${sql}
52
- Full error: ${response.status} ${response.statusText}`
53
- );
48
+ async (response) => {
49
+ throw await parseRemoteError(response);
54
50
  }
55
51
  );
56
52
  let remoteResult;
@@ -58,11 +54,7 @@ Full error: ${response.status} ${response.statusText}`
58
54
  const json = await res.json();
59
55
  remoteResult = remoteResultSchema.parse(json);
60
56
  } catch (e) {
61
- throw new Error(
62
- `Failed to execute query.
63
- Query: ${sql}
64
- Full error: Unexpected JSON response. ${e instanceof Error ? e.message : String(e)}`
65
- );
57
+ throw new AstroDbError(await getUnexpectedResponseMessage(res));
66
58
  }
67
59
  if (method === "run")
68
60
  return remoteResult;
@@ -89,11 +81,8 @@ Full error: Unexpected JSON response. ${e instanceof Error ? e.message : String(
89
81
  },
90
82
  body: JSON.stringify(stmts)
91
83
  },
92
- (response) => {
93
- throw new Error(
94
- `Failed to execute batch queries.
95
- Full error: ${response.status} ${response.statusText}}`
96
- );
84
+ async (response) => {
85
+ throw await parseRemoteError(response);
97
86
  }
98
87
  );
99
88
  let remoteResults;
@@ -101,10 +90,7 @@ Full error: ${response.status} ${response.statusText}}`
101
90
  const json = await res.json();
102
91
  remoteResults = z.array(remoteResultSchema).parse(json);
103
92
  } catch (e) {
104
- throw new Error(
105
- `Failed to execute batch queries.
106
- Full error: Unexpected JSON response. ${e instanceof Error ? e.message : String(e)}`
107
- );
93
+ throw new AstroDbError(await getUnexpectedResponseMessage(res));
108
94
  }
109
95
  let results = [];
110
96
  for (const [idx, rawResult] of remoteResults.entries()) {
@@ -129,6 +115,33 @@ Full error: Unexpected JSON response. ${e instanceof Error ? e.message : String(
129
115
  applyTransactionNotSupported(db);
130
116
  return db;
131
117
  }
118
+ const errorSchema = z.object({
119
+ success: z.boolean(),
120
+ error: z.object({
121
+ code: z.string(),
122
+ details: z.string().optional()
123
+ })
124
+ });
125
+ const KNOWN_ERROR_CODES = {
126
+ SQL_QUERY_FAILED: "SQL_QUERY_FAILED"
127
+ };
128
+ const getUnexpectedResponseMessage = async (response) => `Unexpected response from remote database:
129
+ (Status ${response.status}) ${await response.text()}`;
130
+ async function parseRemoteError(response) {
131
+ let error;
132
+ try {
133
+ error = errorSchema.parse(await response.json()).error;
134
+ } catch (e) {
135
+ return new AstroDbError(await getUnexpectedResponseMessage(response));
136
+ }
137
+ let details = error.details?.replace(/.*SQLite error: /, "") ?? `(Code ${error.code})
138
+ Error querying remote database.`;
139
+ let hint = `See the Astro DB guide for query and push instructions: https://docs.astro.build/en/guides/astro-db/#query-your-database`;
140
+ if (error.code === KNOWN_ERROR_CODES.SQL_QUERY_FAILED && details.includes("no such table")) {
141
+ hint = `Did you run \`astro db push\` to push your latest table schemas?`;
142
+ }
143
+ return new AstroDbError(details, hint);
144
+ }
132
145
  export {
133
146
  createLocalDatabaseClient,
134
147
  createRemoteDatabaseClient
@@ -2,5 +2,4 @@ export declare const FOREIGN_KEY_DNE_ERROR: (tableName: string) => string;
2
2
  export declare const FOREIGN_KEY_REFERENCES_LENGTH_ERROR: (tableName: string) => string;
3
3
  export declare const FOREIGN_KEY_REFERENCES_EMPTY_ERROR: (tableName: string) => string;
4
4
  export declare const REFERENCE_DNE_ERROR: (columnName: string) => string;
5
- export declare const SEED_ERROR: (error: string) => string;
6
5
  export declare const SEED_DEFAULT_EXPORT_ERROR: (fileName: string) => string;
@@ -19,19 +19,13 @@ const REFERENCE_DNE_ERROR = (columnName) => {
19
19
  columnName
20
20
  )} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
21
21
  };
22
- const SEED_ERROR = (error) => {
23
- return `${red(`Error while seeding database:`)}
24
-
25
- ${error}`;
26
- };
27
22
  const SEED_DEFAULT_EXPORT_ERROR = (fileName) => {
28
- return SEED_ERROR(`Missing default function export in ${bold(fileName)}`);
23
+ return `Missing default function export in ${bold(fileName)}`;
29
24
  };
30
25
  export {
31
26
  FOREIGN_KEY_DNE_ERROR,
32
27
  FOREIGN_KEY_REFERENCES_EMPTY_ERROR,
33
28
  FOREIGN_KEY_REFERENCES_LENGTH_ERROR,
34
29
  REFERENCE_DNE_ERROR,
35
- SEED_DEFAULT_EXPORT_ERROR,
36
- SEED_ERROR
30
+ SEED_DEFAULT_EXPORT_ERROR
37
31
  };
@@ -23,3 +23,4 @@ export declare function asDrizzleTable(name: string, table: DBTable): import("dr
23
23
  };
24
24
  dialect: "sqlite";
25
25
  }>;
26
+ export declare function normalizeDatabaseUrl(envDbUrl: string | undefined, defaultDbUrl: string): string;
@@ -1,3 +1,4 @@
1
+ import { pathToFileURL } from "url";
1
2
  import { sql } from "drizzle-orm";
2
3
  import {
3
4
  customType,
@@ -110,10 +111,21 @@ function handleSerializedSQL(def) {
110
111
  }
111
112
  return def;
112
113
  }
114
+ function normalizeDatabaseUrl(envDbUrl, defaultDbUrl) {
115
+ if (envDbUrl) {
116
+ if (envDbUrl.startsWith("file://")) {
117
+ return envDbUrl;
118
+ }
119
+ return new URL(envDbUrl, pathToFileURL(process.cwd())).toString();
120
+ } else {
121
+ return defaultDbUrl;
122
+ }
123
+ }
113
124
  export {
114
125
  asDrizzleTable,
115
126
  createLocalDatabaseClient,
116
127
  createRemoteDatabaseClient,
117
128
  hasPrimaryKey,
129
+ normalizeDatabaseUrl,
118
130
  seedLocal
119
131
  };
@@ -2,8 +2,9 @@ import { LibsqlError } from "@libsql/client";
2
2
  import { sql } from "drizzle-orm";
3
3
  import { SQLiteAsyncDialect } from "drizzle-orm/sqlite-core";
4
4
  import {} from "../core/types.js";
5
- import { SEED_DEFAULT_EXPORT_ERROR, SEED_ERROR } from "./errors.js";
5
+ import { SEED_DEFAULT_EXPORT_ERROR } from "./errors.js";
6
6
  import { getCreateIndexQueries, getCreateTableQuery } from "./queries.js";
7
+ import { AstroDbError } from "./utils.js";
7
8
  const sqlite = new SQLiteAsyncDialect();
8
9
  async function seedLocal({
9
10
  db,
@@ -18,7 +19,7 @@ async function seedLocal({
18
19
  if (seedFilePath) {
19
20
  const mod = userSeedGlob[seedFilePath];
20
21
  if (!mod.default)
21
- throw new Error(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
22
+ throw new AstroDbError(SEED_DEFAULT_EXPORT_ERROR(seedFilePath));
22
23
  seedFunctions.push(mod.default);
23
24
  }
24
25
  for (const seedFn of integrationSeedFunctions) {
@@ -29,7 +30,8 @@ async function seedLocal({
29
30
  await seed();
30
31
  } catch (e) {
31
32
  if (e instanceof LibsqlError) {
32
- throw new Error(SEED_ERROR(e.message));
33
+ throw new AstroDbError(`Failed to seed database:
34
+ ${e.message}`);
33
35
  }
34
36
  throw e;
35
37
  }
@@ -1,4 +1,8 @@
1
+ import { AstroError } from 'astro/errors';
1
2
  /**
2
3
  * Small wrapper around fetch that throws an error if the response is not OK. Allows for custom error handling as well through the onNotOK callback.
3
4
  */
4
5
  export declare function safeFetch(url: Parameters<typeof fetch>[0], options?: Parameters<typeof fetch>[1], onNotOK?: (response: Response) => void | Promise<void>): Promise<Response>;
6
+ export declare class AstroDbError extends AstroError {
7
+ name: string;
8
+ }
@@ -1,3 +1,4 @@
1
+ import { AstroError } from "astro/errors";
1
2
  async function safeFetch(url, options = {}, onNotOK = () => {
2
3
  throw new Error(`Request to ${url} returned a non-OK status code.`);
3
4
  }) {
@@ -7,6 +8,10 @@ async function safeFetch(url, options = {}, onNotOK = () => {
7
8
  }
8
9
  return response;
9
10
  }
11
+ class AstroDbError extends AstroError {
12
+ name = "Astro DB Error";
13
+ }
10
14
  export {
15
+ AstroDbError,
11
16
  safeFetch
12
17
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrojs/db",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -81,7 +81,7 @@
81
81
  "mocha": "^10.2.0",
82
82
  "typescript": "^5.2.2",
83
83
  "vite": "^5.1.4",
84
- "astro": "4.5.12",
84
+ "astro": "4.5.13",
85
85
  "astro-scripts": "0.0.14"
86
86
  },
87
87
  "scripts": {