diff --git a/.changeset/curvy-donkeys-knock.md b/.changeset/curvy-donkeys-knock.md
deleted file mode 100644
index 0197a800e052..000000000000
--- a/.changeset/curvy-donkeys-knock.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"astro": patch
----
-
-Fixes a regression introduced in v4.4.5 where image optimization did not work in dev mode when a base was configured.
diff --git a/.changeset/few-worms-rush.md b/.changeset/few-worms-rush.md
deleted file mode 100644
index 3ca708f94865..000000000000
--- a/.changeset/few-worms-rush.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"astro": patch
----
-
-Adds auto completion for `astro:` event names when adding or removing event listeners on `document`.
diff --git a/.changeset/purple-tips-camp.md b/.changeset/purple-tips-camp.md
new file mode 100644
index 000000000000..e2f1279b3ef3
--- /dev/null
+++ b/.changeset/purple-tips-camp.md
@@ -0,0 +1,5 @@
+---
+"astro": patch
+---
+
+Fixes jerky scrolling on IOS when using view transitions.
diff --git a/.changeset/smooth-singers-kiss.md b/.changeset/smooth-singers-kiss.md
deleted file mode 100644
index 56d1ea893af4..000000000000
--- a/.changeset/smooth-singers-kiss.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-"@astrojs/node": patch
----
-
-Fixes the `server.host` option to properly listen on all network interfaces when set to `true`
diff --git a/examples/basics/package.json b/examples/basics/package.json
index 86f892f41087..e4946c0b9d90 100644
--- a/examples/basics/package.json
+++ b/examples/basics/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/blog/package.json b/examples/blog/package.json
index c51c8592d4bc..95f9cf22756d 100644
--- a/examples/blog/package.json
+++ b/examples/blog/package.json
@@ -14,6 +14,6 @@
"@astrojs/mdx": "^2.1.1",
"@astrojs/rss": "^4.0.5",
"@astrojs/sitemap": "^3.1.1",
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/component/package.json b/examples/component/package.json
index 1cdb6b33662d..f29368a01892 100644
--- a/examples/component/package.json
+++ b/examples/component/package.json
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
},
"peerDependencies": {
"astro": "^4.0.0"
diff --git a/examples/framework-alpine/package.json b/examples/framework-alpine/package.json
index b203a9e4167f..0c49a23bec6e 100644
--- a/examples/framework-alpine/package.json
+++ b/examples/framework-alpine/package.json
@@ -14,6 +14,6 @@
"@astrojs/alpinejs": "^0.4.0",
"@types/alpinejs": "^3.13.5",
"alpinejs": "^3.13.3",
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/framework-lit/package.json b/examples/framework-lit/package.json
index 6d8d7ff1291a..e5206af4b2e9 100644
--- a/examples/framework-lit/package.json
+++ b/examples/framework-lit/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/lit": "^4.0.1",
"@webcomponents/template-shadowroot": "^0.2.1",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"lit": "^3.1.2"
}
}
diff --git a/examples/framework-multiple/package.json b/examples/framework-multiple/package.json
index 65bb39f7d3e8..7d5cef5ba949 100644
--- a/examples/framework-multiple/package.json
+++ b/examples/framework-multiple/package.json
@@ -16,7 +16,7 @@
"@astrojs/solid-js": "^4.0.1",
"@astrojs/svelte": "^5.2.0",
"@astrojs/vue": "^4.0.8",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"preact": "^10.19.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
diff --git a/examples/framework-preact/package.json b/examples/framework-preact/package.json
index 5f61f5790fc4..efafb58ce98c 100644
--- a/examples/framework-preact/package.json
+++ b/examples/framework-preact/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.1.1",
"@preact/signals": "^1.2.1",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"preact": "^10.19.2"
}
}
diff --git a/examples/framework-react/package.json b/examples/framework-react/package.json
index ad2b47014a5f..fba2fb54fba2 100644
--- a/examples/framework-react/package.json
+++ b/examples/framework-react/package.json
@@ -14,7 +14,7 @@
"@astrojs/react": "^3.0.10",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
diff --git a/examples/framework-solid/package.json b/examples/framework-solid/package.json
index f4a1cd063707..e4e5023f29a9 100644
--- a/examples/framework-solid/package.json
+++ b/examples/framework-solid/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/solid-js": "^4.0.1",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"solid-js": "^1.8.5"
}
}
diff --git a/examples/framework-svelte/package.json b/examples/framework-svelte/package.json
index c4b7e3b43332..b50d0c2e362b 100644
--- a/examples/framework-svelte/package.json
+++ b/examples/framework-svelte/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/svelte": "^5.2.0",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"svelte": "^4.2.5"
}
}
diff --git a/examples/framework-vue/package.json b/examples/framework-vue/package.json
index 5864c5a006a1..453833c1f469 100644
--- a/examples/framework-vue/package.json
+++ b/examples/framework-vue/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/vue": "^4.0.8",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"vue": "^3.3.8"
}
}
diff --git a/examples/hackernews/package.json b/examples/hackernews/package.json
index 3560057d1e79..1022917fbd3d 100644
--- a/examples/hackernews/package.json
+++ b/examples/hackernews/package.json
@@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/node": "^8.2.1",
- "astro": "^4.4.6"
+ "@astrojs/node": "^8.2.3",
+ "astro": "^4.4.9"
}
}
diff --git a/examples/integration/package.json b/examples/integration/package.json
index bd2cd6879db1..aa8f861c9111 100644
--- a/examples/integration/package.json
+++ b/examples/integration/package.json
@@ -15,7 +15,7 @@
],
"scripts": {},
"devDependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
},
"peerDependencies": {
"astro": "^4.0.0"
diff --git a/examples/middleware/package.json b/examples/middleware/package.json
index 9ee9f9fa601b..6931f2506e78 100644
--- a/examples/middleware/package.json
+++ b/examples/middleware/package.json
@@ -12,8 +12,8 @@
"server": "node dist/server/entry.mjs"
},
"dependencies": {
- "@astrojs/node": "^8.2.1",
- "astro": "^4.4.6",
+ "@astrojs/node": "^8.2.3",
+ "astro": "^4.4.9",
"html-minifier": "^4.0.0"
},
"devDependencies": {
diff --git a/examples/minimal/package.json b/examples/minimal/package.json
index 85580d9cd383..daaec679ddcc 100644
--- a/examples/minimal/package.json
+++ b/examples/minimal/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/non-html-pages/package.json b/examples/non-html-pages/package.json
index f00fb4b5b97a..9d81c702904f 100644
--- a/examples/non-html-pages/package.json
+++ b/examples/non-html-pages/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/portfolio/package.json b/examples/portfolio/package.json
index e52d98f04c54..63c6a7c1a47a 100644
--- a/examples/portfolio/package.json
+++ b/examples/portfolio/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
index 592fc7077b67..f30f14b86189 100644
--- a/examples/ssr/package.json
+++ b/examples/ssr/package.json
@@ -12,9 +12,9 @@
"server": "node dist/server/entry.mjs"
},
"dependencies": {
- "@astrojs/node": "^8.2.1",
+ "@astrojs/node": "^8.2.3",
"@astrojs/svelte": "^5.2.0",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"svelte": "^4.2.5"
}
}
diff --git a/examples/starlog/package.json b/examples/starlog/package.json
index 83df3e1f30c6..646a712aea10 100644
--- a/examples/starlog/package.json
+++ b/examples/starlog/package.json
@@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"sass": "^1.69.5",
"sharp": "^0.32.6"
}
diff --git a/examples/view-transitions/package.json b/examples/view-transitions/package.json
index a50877a080aa..9b14f2f865dc 100644
--- a/examples/view-transitions/package.json
+++ b/examples/view-transitions/package.json
@@ -11,7 +11,7 @@
},
"devDependencies": {
"@astrojs/tailwind": "^5.1.0",
- "@astrojs/node": "^8.2.1",
- "astro": "^4.4.6"
+ "@astrojs/node": "^8.2.3",
+ "astro": "^4.4.9"
}
}
diff --git a/examples/with-markdoc/package.json b/examples/with-markdoc/package.json
index 2246568f6c20..d2f4f5b927f1 100644
--- a/examples/with-markdoc/package.json
+++ b/examples/with-markdoc/package.json
@@ -11,7 +11,7 @@
"astro": "astro"
},
"dependencies": {
- "@astrojs/markdoc": "^0.9.0",
- "astro": "^4.4.6"
+ "@astrojs/markdoc": "^0.9.1",
+ "astro": "^4.4.9"
}
}
diff --git a/examples/with-markdown-plugins/package.json b/examples/with-markdown-plugins/package.json
index 14caff03e549..a0d3e4fedcc0 100644
--- a/examples/with-markdown-plugins/package.json
+++ b/examples/with-markdown-plugins/package.json
@@ -12,7 +12,7 @@
},
"dependencies": {
"@astrojs/markdown-remark": "^4.2.1",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"hast-util-select": "^6.0.2",
"rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0",
diff --git a/examples/with-markdown-shiki/package.json b/examples/with-markdown-shiki/package.json
index 2a295880e3ab..b5ac1ec1a71e 100644
--- a/examples/with-markdown-shiki/package.json
+++ b/examples/with-markdown-shiki/package.json
@@ -11,6 +11,6 @@
"astro": "astro"
},
"dependencies": {
- "astro": "^4.4.6"
+ "astro": "^4.4.9"
}
}
diff --git a/examples/with-mdx/package.json b/examples/with-mdx/package.json
index 4ceaf2373c7a..a67bb505190e 100644
--- a/examples/with-mdx/package.json
+++ b/examples/with-mdx/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/mdx": "^2.1.1",
"@astrojs/preact": "^3.1.1",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"preact": "^10.19.2"
}
}
diff --git a/examples/with-nanostores/package.json b/examples/with-nanostores/package.json
index 909328a27997..330464bb0f38 100644
--- a/examples/with-nanostores/package.json
+++ b/examples/with-nanostores/package.json
@@ -13,7 +13,7 @@
"dependencies": {
"@astrojs/preact": "^3.1.1",
"@nanostores/preact": "^0.5.0",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"nanostores": "^0.9.5",
"preact": "^10.19.2"
}
diff --git a/examples/with-tailwindcss/package.json b/examples/with-tailwindcss/package.json
index 3d9a75496793..39a205342d5c 100644
--- a/examples/with-tailwindcss/package.json
+++ b/examples/with-tailwindcss/package.json
@@ -14,7 +14,7 @@
"@astrojs/mdx": "^2.1.1",
"@astrojs/tailwind": "^5.1.0",
"@types/canvas-confetti": "^1.6.3",
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"autoprefixer": "^10.4.15",
"canvas-confetti": "^1.9.1",
"postcss": "^8.4.28",
diff --git a/examples/with-vitest/package.json b/examples/with-vitest/package.json
index f337edc99d6e..05c636d421ad 100644
--- a/examples/with-vitest/package.json
+++ b/examples/with-vitest/package.json
@@ -12,7 +12,7 @@
"test": "vitest"
},
"dependencies": {
- "astro": "^4.4.6",
+ "astro": "^4.4.9",
"vitest": "^1.3.1"
}
}
diff --git a/packages/astro/CHANGELOG.md b/packages/astro/CHANGELOG.md
index 901e49d22925..7df2f801fa3f 100644
--- a/packages/astro/CHANGELOG.md
+++ b/packages/astro/CHANGELOG.md
@@ -1,5 +1,31 @@
# astro
+## 4.4.9
+
+### Patch Changes
+
+- [#10278](https://github.com/withastro/astro/pull/10278) [`a548a3a99c2835c19662fc38636f92b2bda26614`](https://github.com/withastro/astro/commit/a548a3a99c2835c19662fc38636f92b2bda26614) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc
+
+- [#10280](https://github.com/withastro/astro/pull/10280) [`3488be9b59d1cb65325b0e087c33bcd74aaa4926`](https://github.com/withastro/astro/commit/3488be9b59d1cb65325b0e087c33bcd74aaa4926) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Finalize db API to a shared db/ directory.
+
+## 4.4.8
+
+### Patch Changes
+
+- [#10275](https://github.com/withastro/astro/pull/10275) [`5e3e74b61daa2ba44c761c9ab5745818661a656e`](https://github.com/withastro/astro/commit/5e3e74b61daa2ba44c761c9ab5745818661a656e) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes dev toolbar warning about using the proper loading attributes on images using `data:` URIs
+
+## 4.4.7
+
+### Patch Changes
+
+- [#10274](https://github.com/withastro/astro/pull/10274) [`e556151603a2f0173059d0f98fdcbec0610b48ff`](https://github.com/withastro/astro/commit/e556151603a2f0173059d0f98fdcbec0610b48ff) Thanks [@lilnasy](https://github.com/lilnasy)! - Fixes a regression introduced in v4.4.5 where image optimization did not work in dev mode when a base was configured.
+
+- [#10263](https://github.com/withastro/astro/pull/10263) [`9bdbed723e0aa4243d7d6ee64d1c1df3b75b9aeb`](https://github.com/withastro/astro/commit/9bdbed723e0aa4243d7d6ee64d1c1df3b75b9aeb) Thanks [@martrapp](https://github.com/martrapp)! - Adds auto completion for `astro:` event names when adding or removing event listeners on `document`.
+
+- [#10284](https://github.com/withastro/astro/pull/10284) [`07f89429a1ef5173d3321e0b362a9dc71fc74fe5`](https://github.com/withastro/astro/commit/07f89429a1ef5173d3321e0b362a9dc71fc74fe5) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where in Node SSR, the image endpoint could be used maliciously to reveal unintended information about the underlying system.
+
+ Thanks to Google Security Team for reporting this issue.
+
## 4.4.6
### Patch Changes
diff --git a/packages/astro/package.json b/packages/astro/package.json
index 561ba56dbff8..50ff30518012 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -1,6 +1,6 @@
{
"name": "astro",
- "version": "4.4.6",
+ "version": "4.4.9",
"description": "Astro is a modern site builder with web best practices, performance, and DX front-of-mind.",
"type": "module",
"author": "withastro",
@@ -108,6 +108,7 @@
"dev": "astro-scripts dev --copy-wasm --prebuild \"src/runtime/server/astro-island.ts\" --prebuild \"src/runtime/client/{idle,load,media,only,visible}.ts\" \"src/**/*.{ts,js}\"",
"postbuild": "astro-scripts copy \"src/**/*.astro\" && astro-scripts copy \"src/**/*.wasm\"",
"test": "pnpm run test:node",
+ "test:match": "pnpm run test:node --match",
"test:e2e": "playwright test",
"test:e2e:match": "playwright test -g",
"test:node": "astro-scripts test \"test/**/*.test.js\""
diff --git a/packages/astro/src/assets/build/generate.ts b/packages/astro/src/assets/build/generate.ts
index 1c73c1592cd8..496dc1e6e4a7 100644
--- a/packages/astro/src/assets/build/generate.ts
+++ b/packages/astro/src/assets/build/generate.ts
@@ -119,7 +119,13 @@ export async function generateImagesForPath(
!globalThis.astroAsset.referencedImages?.has(transformsAndPath.originalSrcPath)
) {
try {
- await fs.promises.unlink(getFullImagePath(originalFilePath, env));
+ if (transformsAndPath.originalSrcPath) {
+ env.logger.debug(
+ 'assets',
+ `Deleting ${originalFilePath} as it's not referenced outside of image processing.`
+ );
+ await fs.promises.unlink(getFullImagePath(originalFilePath, env));
+ }
} catch (e) {
/* No-op, it's okay if we fail to delete one of the file, we're not too picky. */
}
diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts
index cabf02a76e6b..4d29a7fadd02 100644
--- a/packages/astro/src/assets/endpoint/node.ts
+++ b/packages/astro/src/assets/endpoint/node.ts
@@ -1,4 +1,7 @@
-import os from 'os';
+/* eslint-disable no-console */
+import os from 'node:os';
+import { isAbsolute } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path';
import { readFile } from 'fs/promises';
import mime from 'mime/lite.js';
@@ -7,23 +10,44 @@ import { getConfiguredImageService } from '../internal.js';
import { etag } from '../utils/etag.js';
import { isRemoteAllowed } from '../utils/remotePattern.js';
// @ts-expect-error
-import { assetsDir, imageConfig } from 'astro:assets';
+import { assetsDir, imageConfig, outDir } from 'astro:assets';
function replaceFileSystemReferences(src: string) {
return os.platform().includes('win32') ? src.replace(/^\/@fs\//, '') : src.replace(/^\/@fs/, '');
}
async function loadLocalImage(src: string, url: URL) {
- const filePath = import.meta.env.DEV
- ? removeQueryString(replaceFileSystemReferences(src))
- : new URL('.' + src, assetsDir);
+ const assetsDirPath = fileURLToPath(assetsDir);
+
+ let fileUrl;
+ if (import.meta.env.DEV) {
+ fileUrl = pathToFileURL(removeQueryString(replaceFileSystemReferences(src)));
+ } else {
+ try {
+ fileUrl = new URL('.' + src, outDir);
+ const filePath = fileURLToPath(fileUrl);
+
+ if (!isAbsolute(filePath) || !filePath.startsWith(assetsDirPath)) {
+ return undefined;
+ }
+ } catch (err: unknown) {
+ return undefined;
+ }
+ }
+
let buffer: Buffer | undefined = undefined;
try {
- buffer = await readFile(filePath);
+ buffer = await readFile(fileUrl);
} catch (e) {
- const sourceUrl = new URL(src, url.origin);
- buffer = await loadRemoteImage(sourceUrl);
+ // Fallback to try to load the file using `fetch`
+ try {
+ const sourceUrl = new URL(src, url.origin);
+ buffer = await loadRemoteImage(sourceUrl);
+ } catch (err: unknown) {
+ console.error('Could not process image request:', err);
+ return undefined;
+ }
}
return buffer;
@@ -58,7 +82,11 @@ export const GET: APIRoute = async ({ request }) => {
const transform = await imageService.parseURL(url, imageConfig);
if (!transform?.src) {
- throw new Error('Incorrect transform returned by `parseURL`');
+ const err = new Error(
+ 'Incorrect transform returned by `parseURL`. Expected a transform with a `src` property.'
+ );
+ console.error('Could not parse image transform from URL:', err);
+ return new Response('Internal Server Error', { status: 500 });
}
let inputBuffer: Buffer | undefined = undefined;
@@ -74,7 +102,7 @@ export const GET: APIRoute = async ({ request }) => {
}
if (!inputBuffer) {
- return new Response('Not Found', { status: 404 });
+ return new Response('Internal Server Error', { status: 500 });
}
const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
@@ -89,8 +117,12 @@ export const GET: APIRoute = async ({ request }) => {
},
});
} catch (err: unknown) {
- // eslint-disable-next-line no-console
console.error('Could not process image request:', err);
- return new Response(`Server Error: ${err}`, { status: 500 });
+ return new Response(
+ import.meta.env.DEV ? `Could not process image request: ${err}` : `Internal Server Error`,
+ {
+ status: 500,
+ }
+ );
}
};
diff --git a/packages/astro/src/assets/utils/proxy.ts b/packages/astro/src/assets/utils/proxy.ts
index 2b4389e4d142..e5c7ce7a094d 100644
--- a/packages/astro/src/assets/utils/proxy.ts
+++ b/packages/astro/src/assets/utils/proxy.ts
@@ -11,7 +11,11 @@ export function getProxyCode(options: ImageMetadata, isSSR: boolean): string {
if (name === 'fsPath') {
return ${stringifiedFSPath};
}
- ${!isSSR ? `globalThis.astroAsset.referencedImages.add(${stringifiedFSPath});` : ''}
+ ${
+ !isSSR
+ ? `if (target[name] !== undefined) globalThis.astroAsset.referencedImages.add(${stringifiedFSPath});`
+ : ''
+ }
return target[name];
}
})
diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index f531270e3b73..28fcbef39a10 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -67,13 +67,14 @@ export default function assets({
export { default as Picture } from "astro/components/Picture.astro";
export const imageConfig = ${JSON.stringify(settings.config.image)};
- export const assetsDir = new URL(${JSON.stringify(
+ export const outDir = new URL(${JSON.stringify(
new URL(
isServerLikeOutput(settings.config)
? settings.config.build.client
: settings.config.outDir
)
)});
+ export const assetsDir = new URL(${JSON.stringify(settings.config.build.assets)}, outDir);
export const getImage = async (options) => await getImageInternal(options, imageConfig);
`;
}
diff --git a/packages/astro/src/core/config/schema.ts b/packages/astro/src/core/config/schema.ts
index ceb65ba2138e..344318f4e15e 100644
--- a/packages/astro/src/core/config/schema.ts
+++ b/packages/astro/src/core/config/schema.ts
@@ -108,7 +108,6 @@ export const AstroConfigSchema = z.object({
.optional()
.default('attribute'),
adapter: z.object({ name: z.string(), hooks: z.object({}).passthrough().default({}) }).optional(),
- db: z.object({}).passthrough().default({}).optional(),
integrations: z.preprocess(
// preprocess
(val) => (Array.isArray(val) ? val.flat(Infinity).filter(Boolean) : val),
diff --git a/packages/astro/src/core/logger/core.ts b/packages/astro/src/core/logger/core.ts
index 3f9c9f4177b5..45ab41ec92ce 100644
--- a/packages/astro/src/core/logger/core.ts
+++ b/packages/astro/src/core/logger/core.ts
@@ -28,6 +28,7 @@ export type LoggerLabel =
| 'preferences'
| 'redirects'
| 'toolbar'
+ | 'assets'
// SKIP_FORMAT: A special label that tells the logger not to apply any formatting.
// Useful for messages that are already formatted, like the server start message.
| 'SKIP_FORMAT';
diff --git a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts
index 197553a25fc7..4b67fcbf5575 100644
--- a/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts
+++ b/packages/astro/src/runtime/client/dev-toolbar/apps/audit/perf.ts
@@ -38,6 +38,9 @@ export const perf: AuditRuleWithSelector[] = [
// Ignore elements that are above the fold, they should be loaded eagerly
if (htmlElement.offsetTop < window.innerHeight) return false;
+ // Ignore elements using `data:` URI, the `loading` attribute doesn't do anything for these
+ if (htmlElement.src.startsWith('data:')) return false;
+
return true;
},
},
@@ -53,6 +56,9 @@ export const perf: AuditRuleWithSelector[] = [
// Ignore elements that are below the fold, they should be loaded lazily
if (htmlElement.offsetTop > window.innerHeight) return false;
+ // Ignore elements using `data:` URI, the `loading` attribute doesn't do anything for these
+ if (htmlElement.src.startsWith('data:')) return false;
+
return true;
},
},
diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts
index 6ca3f666a871..02e2d69dfd20 100644
--- a/packages/astro/src/transitions/router.ts
+++ b/packages/astro/src/transitions/router.ts
@@ -90,28 +90,6 @@ if (inBrowser) {
}
}
-const throttle = (cb: (...args: any[]) => any, delay: number) => {
- let wait = false;
- // During the waiting time additional events are lost.
- // So repeat the callback at the end if we have swallowed events.
- let onceMore = false;
- return (...args: any[]) => {
- if (wait) {
- onceMore = true;
- return;
- }
- cb(...args);
- wait = true;
- setTimeout(() => {
- if (onceMore) {
- onceMore = false;
- cb(...args);
- }
- wait = false;
- }, delay);
- };
-};
-
// returns the contents of the page or null if the router can't deal with it.
async function fetchHTML(
href: string,
@@ -625,10 +603,15 @@ function onPopState(ev: PopStateEvent) {
transition(direction, originalLocation, new URL(location.href), {}, state);
}
-// There's not a good way to record scroll position before a back button.
-// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
-const onScroll = () => {
- updateScrollPosition({ scrollX, scrollY });
+const onScrollEnd = () => {
+ // NOTE: our "popstate" event handler may call `pushState()` or
+ // `replaceState()` and then `scrollTo()`, which will fire "scroll" and
+ // "scrollend" events. To avoid redundant work and expensive calls to
+ // `replaceState()`, we simply check that the values are different before
+ // updating.
+ if (scrollX !== history.state.scrollX || scrollY !== history.state.scrollY) {
+ updateScrollPosition({ scrollX, scrollY });
+ }
};
// initialization
@@ -637,8 +620,42 @@ if (inBrowser) {
originalLocation = new URL(location.href);
addEventListener('popstate', onPopState);
addEventListener('load', onPageLoad);
- if ('onscrollend' in window) addEventListener('scrollend', onScroll);
- else addEventListener('scroll', throttle(onScroll, 350), { passive: true });
+ // There's not a good way to record scroll position before a history back
+ // navigation, so we will record it when the user has stopped scrolling.
+ if ('onscrollend' in window) addEventListener('scrollend', onScrollEnd);
+ else {
+ // Keep track of state between intervals
+ let intervalId: number | undefined, lastY: number, lastX: number, lastIndex: State['index'];
+ const scrollInterval = () => {
+ // Check the index to see if a popstate event was fired
+ if (lastIndex !== history.state?.index) {
+ clearInterval(intervalId);
+ intervalId = undefined;
+ return;
+ }
+ // Check if the user stopped scrolling
+ if (lastY === scrollY && lastX === scrollX) {
+ // Cancel the interval and update scroll positions
+ clearInterval(intervalId);
+ intervalId = undefined;
+ onScrollEnd();
+ return;
+ } else {
+ // Update vars with current positions
+ (lastY = scrollY), (lastX = scrollX);
+ }
+ };
+ // We can't know when or how often scroll events fire, so we'll just use them to start intervals
+ addEventListener(
+ 'scroll',
+ () => {
+ if (intervalId !== undefined) return;
+ (lastIndex = history.state.index), (lastY = scrollY), (lastX = scrollX);
+ intervalId = window.setInterval(scrollInterval, 50);
+ },
+ { passive: true }
+ );
+ }
}
for (const script of document.scripts) {
script.dataset.astroExec = '';
diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js
index 590e77000557..3f53fba96180 100644
--- a/packages/astro/test/core-image.test.js
+++ b/packages/astro/test/core-image.test.js
@@ -1103,6 +1103,96 @@ describe('astro:image', () => {
assert.equal(response.status, 200);
});
+ it('endpoint handle malformed requests', async () => {
+ const badPaths = [
+ '../../../../../../../../../../../../etc/hosts%00',
+ '../../../../../../../../../../../../etc/hosts',
+ '../../boot.ini',
+ '/../../../../../../../../%2A',
+ '../../../../../../../../../../../../etc/passwd%00',
+ '../../../../../../../../../../../../etc/passwd',
+ '../../../../../../../../../../../../etc/shadow%00',
+ '../../../../../../../../../../../../etc/shadow',
+ '/../../../../../../../../../../etc/passwd^^',
+ '/../../../../../../../../../../etc/shadow^^',
+ '/../../../../../../../../../../etc/passwd',
+ '/../../../../../../../../../../etc/shadow',
+ '/./././././././././././etc/passwd',
+ '/./././././././././././etc/shadow',
+ '....................etcpasswd',
+ '....................etcshadow',
+ '....................etcpasswd',
+ '....................etcshadow',
+ '/..../..../..../..../..../..../etc/passwd',
+ '/..../..../..../..../..../..../etc/shadow',
+ '.\\./.\\./.\\./.\\./.\\./.\\./etc/passwd',
+ '.\\./.\\./.\\./.\\./.\\./.\\./etc/shadow',
+ '....................etcpasswd%00',
+ '....................etcshadow%00',
+ '....................etcpasswd%00',
+ '....................etcshadow%00',
+ '%0a/bin/cat%20/etc/passwd',
+ '%0a/bin/cat%20/etc/shadow',
+ '%00/etc/passwd%00',
+ '%00/etc/shadow%00',
+ '%00../../../../../../etc/passwd',
+ '%00../../../../../../etc/shadow',
+ '/../../../../../../../../../../../etc/passwd%00.jpg',
+ '/../../../../../../../../../../../etc/passwd%00.html',
+ '/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/passwd',
+ '/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/shadow',
+ '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd,',
+ '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/shadow,',
+ '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..%25%5c..%25%5c..%00',
+ '/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..%00',
+ '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%00',
+ '%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%,25%5c..%25%5c..% 25%5c..%25%5c..%255cboot.ini',
+ '/%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..%25%5c..,%25%5c..%25%5c..%25%5c..%25%5c..winnt/desktop.ini',
+ '\\'/bin/cat%20/etc/passwd\\'',
+ '\\'/bin/cat%20/etc/shadow\\'',
+ '../../../../../../../../conf/server.xml',
+ '/../../../../../../../../bin/id|',
+ 'C:/inetpub/wwwroot/global.asa',
+ 'C:inetpubwwwrootglobal.asa',
+ 'C:/boot.ini',
+ 'C:\boot.ini',
+ '../../../../../../../../../../../../localstart.asp%00',
+ '../../../../../../../../../../../../localstart.asp',
+ '../../../../../../../../../../../../boot.ini%00',
+ '../../../../../../../../../../../../boot.ini',
+ '/./././././././././././boot.ini',
+ '/../../../../../../../../../../../boot.ini%00',
+ '/../../../../../../../../../../../boot.ini',
+ '/..../..../..../..../..../..../boot.ini',
+ '/.\\./.\\./.\\./.\\./.\\./.\\./boot.ini',
+ '....................\boot.ini',
+ '....................\boot.ini%00',
+ '....................\boot.ini',
+ '/../../../../../../../../../../../boot.ini%00.html',
+ '/../../../../../../../../../../../boot.ini%00.jpg',
+ '/.../.../.../.../.../ ',
+ '..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../boot.ini',
+ '/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/boot.ini',
+ '../prerender/index.html',
+ ];
+
+ const app = await fixture.loadTestAdapterApp();
+
+ for (const path of badPaths) {
+ let request = new Request('http://example.com/_image?href=' + path);
+ let response = await app.render(request);
+ const body = await response.text();
+
+ assert.equal(response.status, 500);
+ assert.equal(body.includes('Internal Server Error'), true);
+ }
+
+ // Server should still be running
+ let request = new Request('http://example.com/');
+ let response = await app.render(request);
+ assert.equal(response.status, 200);
+ });
+
it('prerendered routes images are built', async () => {
const html = await fixture.readFile('/client/prerender/index.html');
const $ = cheerio.load(html);
diff --git a/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs
new file mode 100644
index 000000000000..8ba730fc91bc
--- /dev/null
+++ b/packages/astro/test/fixtures/core-image-deletion/astro.config.mjs
@@ -0,0 +1,7 @@
+import markdoc from '@astrojs/markdoc';
+import mdx from '@astrojs/mdx';
+import { defineConfig } from 'astro/config';
+
+export default defineConfig({
+ integrations: [mdx(), markdoc()],
+});
diff --git a/packages/astro/test/fixtures/core-image-deletion/package.json b/packages/astro/test/fixtures/core-image-deletion/package.json
index 40a2ee1a64b1..e55fc847dce8 100644
--- a/packages/astro/test/fixtures/core-image-deletion/package.json
+++ b/packages/astro/test/fixtures/core-image-deletion/package.json
@@ -3,7 +3,9 @@
"version": "0.0.0",
"private": true,
"dependencies": {
- "astro": "workspace:*"
+ "astro": "workspace:*",
+ "@astrojs/mdx": "workspace:*",
+ "@astrojs/markdoc": "workspace:*"
},
"scripts": {
"dev": "astro dev"
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg b/packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg
new file mode 100644
index 000000000000..8e38327233fc
Binary files /dev/null and b/packages/astro/test/fixtures/core-image-deletion/src/assets/markdocStillExists.jpg differ
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg b/packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg
new file mode 100644
index 000000000000..8c5ba062c2f0
Binary files /dev/null and b/packages/astro/test/fixtures/core-image-deletion/src/assets/mdxDontExist.jpg differ
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/content/blog/markdoc.mdoc b/packages/astro/test/fixtures/core-image-deletion/src/content/blog/markdoc.mdoc
new file mode 100644
index 000000000000..41119a9e7a53
--- /dev/null
+++ b/packages/astro/test/fixtures/core-image-deletion/src/content/blog/markdoc.mdoc
@@ -0,0 +1,7 @@
+---
+title: "Markdoc"
+---
+
+There is an image below
+
+![A penguin](../../assets/markdocStillExists.jpg)
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/content/blog/mdx.mdx b/packages/astro/test/fixtures/core-image-deletion/src/content/blog/mdx.mdx
new file mode 100644
index 000000000000..3de8bda4ffe6
--- /dev/null
+++ b/packages/astro/test/fixtures/core-image-deletion/src/content/blog/mdx.mdx
@@ -0,0 +1,7 @@
+---
+title: "MDX"
+---
+
+There is an image below
+
+![A penguin](../../assets/mdxDontExist.jpg)
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/content/config.ts b/packages/astro/test/fixtures/core-image-deletion/src/content/config.ts
new file mode 100644
index 000000000000..1c0eae662372
--- /dev/null
+++ b/packages/astro/test/fixtures/core-image-deletion/src/content/config.ts
@@ -0,0 +1,12 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ type: 'content',
+ schema: z.object({
+ title: z.string(),
+ })
+});
+
+export const collections = {
+ blog: blog,
+};
diff --git a/packages/astro/test/fixtures/core-image-deletion/src/pages/blog/[slug].astro b/packages/astro/test/fixtures/core-image-deletion/src/pages/blog/[slug].astro
new file mode 100644
index 000000000000..418d449bb64c
--- /dev/null
+++ b/packages/astro/test/fixtures/core-image-deletion/src/pages/blog/[slug].astro
@@ -0,0 +1,22 @@
+---
+import type { GetStaticPaths } from "astro";
+import { getCollection } from "astro:content";
+
+export const getStaticPaths = (async () => {
+ const blog = await getCollection("blog");
+ return blog.map((post) => ({
+ params: {
+ slug: post.slug,
+ },
+ props: {
+ post
+ }
+ }));
+}) satisfies GetStaticPaths;
+
+const { post } = Astro.props;
+
+const { Content } = await post.render();
+---
+
+
diff --git a/packages/astro/test/image-deletion.test.js b/packages/astro/test/image-deletion.test.js
index 4283e8c06f3b..cb4b464bf6aa 100644
--- a/packages/astro/test/image-deletion.test.js
+++ b/packages/astro/test/image-deletion.test.js
@@ -3,7 +3,7 @@ import { before, describe, it } from 'node:test';
import { testImageService } from './test-image-service.js';
import { loadFixture } from './test-utils.js';
-describe('astro:assets - delete images that are unused', () => {
+describe('astro:assets - delete images that are unused zzz', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;
@@ -33,5 +33,15 @@ describe('astro:assets - delete images that are unused', () => {
const imagesUsedElsewhere = await fixture.glob('_astro/url.*.*');
assert.equal(imagesUsedElsewhere.length, 2);
});
+
+ it('should delete MDX images only used for optimization', async () => {
+ const imagesOnlyOptimized = await fixture.glob('_astro/mdxDontExist.*.*');
+ assert.equal(imagesOnlyOptimized.length, 1);
+ });
+
+ it('should always keep Markdoc images', async () => {
+ const imagesUsedElsewhere = await fixture.glob('_astro/markdocStillExists.*.*');
+ assert.equal(imagesUsedElsewhere.length, 2);
+ });
});
});
diff --git a/packages/db/CHANGELOG.md b/packages/db/CHANGELOG.md
index 75d993f3de00..0f9a08f2b64a 100644
--- a/packages/db/CHANGELOG.md
+++ b/packages/db/CHANGELOG.md
@@ -1,5 +1,11 @@
# @astrojs/db
+## 0.5.0
+
+### Minor Changes
+
+- [#10280](https://github.com/withastro/astro/pull/10280) [`3488be9b59d1cb65325b0e087c33bcd74aaa4926`](https://github.com/withastro/astro/commit/3488be9b59d1cb65325b0e087c33bcd74aaa4926) Thanks [@bholmesdev](https://github.com/bholmesdev)! - Finalize db API to a shared db/ directory.
+
## 0.4.1
### Patch Changes
diff --git a/packages/db/config-augment.d.ts b/packages/db/config-augment.d.ts
deleted file mode 100644
index 3278e6c2abd1..000000000000
--- a/packages/db/config-augment.d.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare namespace Config {
- type DBUserConfig = import('./dist/core/types.js').DBUserConfig;
- export interface Database extends DBUserConfig {}
-}
diff --git a/packages/db/index.d.ts b/packages/db/index.d.ts
index 3a2134b9fc22..81af4fb43cfc 100644
--- a/packages/db/index.d.ts
+++ b/packages/db/index.d.ts
@@ -1,3 +1,5 @@
-///
-export * from './dist/index.js';
-export { default } from './dist/index.js';
+export { default, cli } from './dist/index.js';
+
+declare module 'astro:db' {
+ export { defineTable, defineDB, column, sql, NOW, TRUE, FALSE } from './dist/index.js';
+}
diff --git a/packages/db/package.json b/packages/db/package.json
index b2055f20d452..407f938973e5 100644
--- a/packages/db/package.json
+++ b/packages/db/package.json
@@ -1,6 +1,6 @@
{
"name": "@astrojs/db",
- "version": "0.4.1",
+ "version": "0.5.0",
"description": "",
"license": "MIT",
"type": "module",
@@ -12,6 +12,10 @@
"types": "./index.d.ts",
"import": "./dist/index.js"
},
+ "./utils": {
+ "types": "./dist/utils.d.ts",
+ "import": "./dist/utils.js"
+ },
"./runtime": {
"types": "./dist/runtime/index.d.ts",
"import": "./dist/runtime/index.js"
@@ -20,6 +24,10 @@
"types": "./dist/runtime/drizzle.d.ts",
"import": "./dist/runtime/drizzle.js"
},
+ "./runtime/config": {
+ "types": "./dist/runtime/config.d.ts",
+ "import": "./dist/runtime/config.js"
+ },
"./package.json": "./package.json"
},
"typesVersions": {
@@ -27,11 +35,17 @@
".": [
"./index.d.ts"
],
+ "utils": [
+ "./dist/utils.d.ts"
+ ],
"runtime": [
"./dist/runtime/index.d.ts"
],
"runtime/drizzle": [
"./dist/runtime/drizzle.d.ts"
+ ],
+ "runtime/config": [
+ "./dist/runtime/config.d.ts"
]
}
},
diff --git a/packages/db/src/core/cli/commands/execute/index.ts b/packages/db/src/core/cli/commands/execute/index.ts
new file mode 100644
index 000000000000..1863691e706e
--- /dev/null
+++ b/packages/db/src/core/cli/commands/execute/index.ts
@@ -0,0 +1,40 @@
+import { existsSync } from 'node:fs';
+import type { AstroConfig } from 'astro';
+import type { Arguments } from 'yargs-parser';
+import { FILE_NOT_FOUND_ERROR, MISSING_EXECUTE_PATH_ERROR } from '../../../errors.js';
+import { getStudioVirtualModContents } from '../../../integration/vite-plugin-db.js';
+import { bundleFile, importBundledFile } from '../../../load-file.js';
+import { getManagedAppTokenOrExit } from '../../../tokens.js';
+import { type DBConfig } from '../../../types.js';
+
+export async function cmd({
+ astroConfig,
+ dbConfig,
+ flags,
+}: {
+ astroConfig: AstroConfig;
+ dbConfig: DBConfig;
+ flags: Arguments;
+}) {
+ const filePath = flags._[4];
+ if (typeof filePath !== 'string') {
+ console.error(MISSING_EXECUTE_PATH_ERROR);
+ process.exit(1);
+ }
+
+ const fileUrl = new URL(filePath, astroConfig.root);
+ if (!existsSync(fileUrl)) {
+ console.error(FILE_NOT_FOUND_ERROR(filePath));
+ process.exit(1);
+ }
+
+ const appToken = await getManagedAppTokenOrExit(flags.token);
+
+ const virtualModContents = getStudioVirtualModContents({
+ tables: dbConfig.tables ?? {},
+ appToken: appToken.token,
+ });
+ const { code } = await bundleFile({ virtualModContents, root: astroConfig.root, fileUrl });
+ // Executable files use top-level await. Importing will run the file.
+ await importBundledFile({ code, root: astroConfig.root });
+}
diff --git a/packages/db/src/core/cli/commands/gen/index.ts b/packages/db/src/core/cli/commands/gen/index.ts
index 25f59be45a83..be157d4a5545 100644
--- a/packages/db/src/core/cli/commands/gen/index.ts
+++ b/packages/db/src/core/cli/commands/gen/index.ts
@@ -1,7 +1,11 @@
import { writeFile } from 'node:fs/promises';
+import { relative } from 'node:path';
+import { fileURLToPath } from 'node:url';
import type { AstroConfig } from 'astro';
-import { bgRed, red, reset } from 'kleur/colors';
+import { bgRed, bold, red, reset } from 'kleur/colors';
import type { Arguments } from 'yargs-parser';
+import type { DBConfig } from '../../../types.js';
+import { getMigrationsDirectoryUrl } from '../../../utils.js';
import { getMigrationQueries } from '../../migration-queries.js';
import {
MIGRATIONS_CREATED,
@@ -10,11 +14,19 @@ import {
initializeMigrationsDirectory,
} from '../../migrations.js';
-export async function cmd({ config }: { config: AstroConfig; flags: Arguments }) {
- const migration = await getMigrationStatus(config);
+export async function cmd({
+ astroConfig,
+ dbConfig,
+}: {
+ astroConfig: AstroConfig;
+ dbConfig: DBConfig;
+ flags: Arguments;
+}) {
+ const migration = await getMigrationStatus({ dbConfig, root: astroConfig.root });
+ const migrationsDir = getMigrationsDirectoryUrl(astroConfig.root);
if (migration.state === 'no-migrations-found') {
- await initializeMigrationsDirectory(migration.currentSnapshot);
+ await initializeMigrationsDirectory(migration.currentSnapshot, migrationsDir);
console.log(MIGRATIONS_CREATED);
return;
} else if (migration.state === 'up-to-date') {
@@ -30,14 +42,15 @@ export async function cmd({ config }: { config: AstroConfig; flags: Arguments })
// Warn the user about any changes that lead to data-loss.
// When the user runs `db push`, they will be prompted to confirm these changes.
confirmations.map((message) => console.log(bgRed(' !!! ') + ' ' + red(message)));
- const migrationFileContent = {
+ const content = {
diff,
db: migrationQueries,
// TODO(fks): Encode the relevant data, instead of the raw message.
// This will give `db push` more control over the formatting of the message.
confirm: confirmations.map((c) => reset(c)),
};
- const migrationFileName = `./migrations/${newFilename}`;
- await writeFile(migrationFileName, JSON.stringify(migrationFileContent, undefined, 2));
- console.log(migrationFileName + ' created!');
+ const fileUrl = new URL(newFilename, migrationsDir);
+ const relativePath = relative(fileURLToPath(astroConfig.root), fileURLToPath(fileUrl));
+ await writeFile(fileUrl, JSON.stringify(content, undefined, 2));
+ console.log(bold(relativePath) + ' created!');
}
diff --git a/packages/db/src/core/cli/commands/link/index.ts b/packages/db/src/core/cli/commands/link/index.ts
index 4acfa7ec4849..f92a1818ca41 100644
--- a/packages/db/src/core/cli/commands/link/index.ts
+++ b/packages/db/src/core/cli/commands/link/index.ts
@@ -1,17 +1,15 @@
import { mkdir, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename } from 'node:path';
-import type { AstroConfig } from 'astro';
import { slug } from 'github-slugger';
import { bgRed, cyan } from 'kleur/colors';
import ora from 'ora';
import prompts from 'prompts';
-import type { Arguments } from 'yargs-parser';
import { MISSING_SESSION_ID_ERROR } from '../../../errors.js';
import { PROJECT_ID_FILE, getSessionIdFromFile } from '../../../tokens.js';
import { getAstroStudioUrl } from '../../../utils.js';
-export async function cmd({}: { config: AstroConfig; flags: Arguments }) {
+export async function cmd() {
const sessionToken = await getSessionIdFromFile();
if (!sessionToken) {
console.error(MISSING_SESSION_ID_ERROR);
diff --git a/packages/db/src/core/cli/commands/login/index.ts b/packages/db/src/core/cli/commands/login/index.ts
index 215d5723e158..2a3f16447d87 100644
--- a/packages/db/src/core/cli/commands/login/index.ts
+++ b/packages/db/src/core/cli/commands/login/index.ts
@@ -7,6 +7,7 @@ import open from 'open';
import ora from 'ora';
import type { Arguments } from 'yargs-parser';
import { SESSION_LOGIN_FILE } from '../../../tokens.js';
+import type { DBConfig } from '../../../types.js';
import { getAstroStudioUrl } from '../../../utils.js';
// NOTE(fks): How the Astro CLI login process works:
@@ -47,7 +48,13 @@ async function createServer(): Promise<{ url: string; promise: Promise }
return { url: serverUrl, promise: sessionPromise };
}
-export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) {
+export async function cmd({
+ flags,
+}: {
+ astroConfig: AstroConfig;
+ dbConfig: DBConfig;
+ flags: Arguments;
+}) {
let session = flags.session;
if (!session) {
diff --git a/packages/db/src/core/cli/commands/logout/index.ts b/packages/db/src/core/cli/commands/logout/index.ts
index 0881dcf4c82e..fbc4ba78cd83 100644
--- a/packages/db/src/core/cli/commands/logout/index.ts
+++ b/packages/db/src/core/cli/commands/logout/index.ts
@@ -1,9 +1,7 @@
import { unlink } from 'node:fs/promises';
-import type { AstroConfig } from 'astro';
-import type { Arguments } from 'yargs-parser';
import { SESSION_LOGIN_FILE } from '../../../tokens.js';
-export async function cmd({}: { config: AstroConfig; flags: Arguments }) {
+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 8c3f21e4d773..81d232cb2904 100644
--- a/packages/db/src/core/cli/commands/push/index.ts
+++ b/packages/db/src/core/cli/commands/push/index.ts
@@ -1,18 +1,14 @@
-import { type InStatement, createClient } from '@libsql/client';
import type { AstroConfig } from 'astro';
-import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
-import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
-import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy';
import { red } from 'kleur/colors';
import prompts from 'prompts';
import type { Arguments } from 'yargs-parser';
import { MISSING_SESSION_ID_ERROR } from '../../../errors.js';
-import { recreateTables, seedData } from '../../../queries.js';
import { getManagedAppTokenOrExit } from '../../../tokens.js';
-import { type AstroConfigWithDB, type DBSnapshot, tablesSchema } from '../../../types.js';
-import { getRemoteDatabaseUrl } from '../../../utils.js';
+import { type DBConfig, type DBSnapshot } from '../../../types.js';
+import { getMigrationsDirectoryUrl, getRemoteDatabaseUrl } from '../../../utils.js';
import { getMigrationQueries } from '../../migration-queries.js';
import {
+ INITIAL_SNAPSHOT,
MIGRATIONS_NOT_INITIALIZED,
MIGRATIONS_UP_TO_DATE,
MIGRATION_NEEDED,
@@ -23,10 +19,18 @@ import {
loadMigration,
} from '../../migrations.js';
-export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) {
+export async function cmd({
+ astroConfig,
+ dbConfig,
+ flags,
+}: {
+ astroConfig: AstroConfig;
+ dbConfig: DBConfig;
+ flags: Arguments;
+}) {
const isDryRun = flags.dryRun;
const appToken = await getManagedAppTokenOrExit(flags.token);
- const migration = await getMigrationStatus(config);
+ const migration = await getMigrationStatus({ dbConfig, root: astroConfig.root });
if (migration.state === 'no-migrations-found') {
console.log(MIGRATIONS_NOT_INITIALIZED);
process.exit(1);
@@ -34,9 +38,10 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
console.log(MIGRATION_NEEDED);
process.exit(1);
}
+ const migrationsDir = getMigrationsDirectoryUrl(astroConfig.root);
// get all migrations from the filesystem
- const allLocalMigrations = await getMigrations();
+ const allLocalMigrations = await getMigrations(migrationsDir);
let missingMigrations: string[] = [];
try {
const { data } = await prepareMigrateQuery({
@@ -63,14 +68,12 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
console.log(`Pushing ${missingMigrations.length} migrations...`);
await pushSchema({
migrations: missingMigrations,
+ migrationsDir,
appToken: appToken.token,
isDryRun,
currentSnapshot: migration.currentSnapshot,
});
}
- // push the database seed data
- console.info('Pushing data...');
- await pushData({ config, appToken: appToken.token, isDryRun });
// cleanup and exit
await appToken.destroy();
console.info('Push complete!');
@@ -78,25 +81,29 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum
async function pushSchema({
migrations,
+ migrationsDir,
appToken,
isDryRun,
currentSnapshot,
}: {
migrations: string[];
+ migrationsDir: URL;
appToken: string;
isDryRun: boolean;
currentSnapshot: DBSnapshot;
}) {
// load all missing migrations
- const initialSnapshot = migrations.find((m) => m === '0000_snapshot.json');
- const filteredMigrations = migrations.filter((m) => m !== '0000_snapshot.json');
- const missingMigrationContents = await Promise.all(filteredMigrations.map(loadMigration));
+ const initialSnapshot = migrations.find((m) => m === INITIAL_SNAPSHOT);
+ const filteredMigrations = migrations.filter((m) => m !== INITIAL_SNAPSHOT);
+ const missingMigrationContents = await Promise.all(
+ filteredMigrations.map((m) => loadMigration(m, migrationsDir))
+ );
// create a migration for the initial snapshot, if needed
const initialMigrationBatch = initialSnapshot
? (
await getMigrationQueries({
oldSnapshot: createEmptySnapshot(),
- newSnapshot: await loadInitialSnapshot(),
+ newSnapshot: await loadInitialSnapshot(migrationsDir),
})
).queries
: [];
@@ -130,76 +137,6 @@ async function pushSchema({
await runMigrateQuery({ queries, migrations, snapshot: currentSnapshot, appToken, isDryRun });
}
-const sqlite = new SQLiteAsyncDialect();
-
-async function pushData({
- config,
- appToken,
- isDryRun,
-}: {
- config: AstroConfigWithDB;
- appToken: string;
- isDryRun?: boolean;
-}) {
- const queries: InStatement[] = [];
- if (config.db?.data) {
- const libsqlClient = createClient({ url: ':memory:' });
- // Stand up tables locally to mirror inserts.
- // Needed to generate return values.
- await recreateTables({
- db: drizzleLibsql(libsqlClient),
- tables: tablesSchema.parse(config.db.tables ?? {}),
- });
-
- for (const [collectionName, { writable }] of Object.entries(config.db.tables ?? {})) {
- if (!writable) {
- queries.push({
- sql: `DELETE FROM ${sqlite.escapeName(collectionName)}`,
- args: [],
- });
- }
- }
-
- // Use proxy to trace all queries to queue up in a batch.
- const db = await drizzleProxy(async (sqlQuery, params, method) => {
- const stmt: InStatement = { sql: sqlQuery, args: params };
- queries.push(stmt);
- // Use in-memory database to generate results for `returning()`.
- const { rows } = await libsqlClient.execute(stmt);
- const rowValues: unknown[][] = [];
- for (const row of rows) {
- if (row != null && typeof row === 'object') {
- rowValues.push(Object.values(row));
- }
- }
- if (method === 'get') {
- return { rows: rowValues[0] };
- }
- return { rows: rowValues };
- });
- await seedData({
- db,
- mode: 'build',
- data: config.db.data,
- });
- }
-
- const url = new URL('/db/query', getRemoteDatabaseUrl());
-
- if (isDryRun) {
- console.info('[DRY RUN] Batch data seed:', JSON.stringify(queries, null, 2));
- return new Response(null, { status: 200 });
- }
-
- return await fetch(url, {
- method: 'POST',
- headers: new Headers({
- Authorization: `Bearer ${appToken}`,
- }),
- body: JSON.stringify(queries),
- });
-}
-
async function runMigrateQuery({
queries: baseQueries,
migrations,
diff --git a/packages/db/src/core/cli/commands/shell/index.ts b/packages/db/src/core/cli/commands/shell/index.ts
index 13450bf31144..ef54b6b70db2 100644
--- a/packages/db/src/core/cli/commands/shell/index.ts
+++ b/packages/db/src/core/cli/commands/shell/index.ts
@@ -3,9 +3,16 @@ import { sql } from 'drizzle-orm';
import type { Arguments } from 'yargs-parser';
import { createRemoteDatabaseClient } from '../../../../runtime/db-client.js';
import { getManagedAppTokenOrExit } from '../../../tokens.js';
+import type { DBConfigInput } from '../../../types.js';
import { getRemoteDatabaseUrl } from '../../../utils.js';
-export async function cmd({ flags }: { config: AstroConfig; flags: Arguments }) {
+export async function cmd({
+ flags,
+}: {
+ dbConfig: DBConfigInput;
+ astroConfig: AstroConfig;
+ flags: Arguments;
+}) {
const query = flags.query;
const appToken = await getManagedAppTokenOrExit(flags.token);
const db = createRemoteDatabaseClient(appToken.token, getRemoteDatabaseUrl());
diff --git a/packages/db/src/core/cli/commands/verify/index.ts b/packages/db/src/core/cli/commands/verify/index.ts
index 6d839d60673c..3b95835f7a64 100644
--- a/packages/db/src/core/cli/commands/verify/index.ts
+++ b/packages/db/src/core/cli/commands/verify/index.ts
@@ -1,5 +1,6 @@
import type { AstroConfig } from 'astro';
import type { Arguments } from 'yargs-parser';
+import type { DBConfig } from '../../../types.js';
import { getMigrationQueries } from '../../migration-queries.js';
import {
MIGRATIONS_NOT_INITIALIZED,
@@ -8,8 +9,16 @@ import {
getMigrationStatus,
} from '../../migrations.js';
-export async function cmd({ config, flags }: { config: AstroConfig; flags: Arguments }) {
- const status = await getMigrationStatus(config);
+export async function cmd({
+ astroConfig,
+ dbConfig,
+ flags,
+}: {
+ astroConfig: AstroConfig;
+ dbConfig: DBConfig;
+ flags: Arguments;
+}) {
+ const status = await getMigrationStatus({ dbConfig, root: astroConfig.root });
const { state } = status;
if (flags.json) {
if (state === 'ahead') {
diff --git a/packages/db/src/core/cli/index.ts b/packages/db/src/core/cli/index.ts
index 5d29b8dda0de..9ffaee7b3059 100644
--- a/packages/db/src/core/cli/index.ts
+++ b/packages/db/src/core/cli/index.ts
@@ -1,50 +1,56 @@
import type { AstroConfig } from 'astro';
import type { Arguments } from 'yargs-parser';
-import { STUDIO_CONFIG_MISSING_CLI_ERROR } from '../errors.js';
+import { loadDbConfigFile } from '../load-file.js';
+import { dbConfigSchema } from '../types.js';
-export async function cli({ flags, config }: { flags: Arguments; config: AstroConfig }) {
+export async function cli({
+ flags,
+ config: astroConfig,
+}: {
+ flags: Arguments;
+ config: AstroConfig;
+}) {
const args = flags._ as string[];
// Most commands are `astro db foo`, but for now login/logout
// are also handled by this package, so first check if this is a db command.
const command = args[2] === 'db' ? args[3] : args[2];
-
- switch (command) {
- case 'login': {
- const { cmd } = await import('./commands/login/index.js');
- return await cmd({ config, flags });
- }
- case 'logout': {
- const { cmd } = await import('./commands/logout/index.js');
- return await cmd({ config, flags });
- }
- }
-
- if (!config.db?.studio) {
- console.log(STUDIO_CONFIG_MISSING_CLI_ERROR);
- process.exit(1);
- }
+ const { mod } = await loadDbConfigFile(astroConfig.root);
+ // TODO: parseConfigOrExit()
+ const dbConfig = dbConfigSchema.parse(mod?.default ?? {});
switch (command) {
case 'shell': {
const { cmd } = await import('./commands/shell/index.js');
- return await cmd({ config, flags });
+ return await cmd({ astroConfig, dbConfig, flags });
}
case 'gen':
case 'sync': {
const { cmd } = await import('./commands/gen/index.js');
- return await cmd({ config, flags });
+ return await cmd({ astroConfig, dbConfig, flags });
}
case 'push': {
const { cmd } = await import('./commands/push/index.js');
- return await cmd({ config, flags });
+ return await cmd({ astroConfig, dbConfig, flags });
}
case 'verify': {
const { cmd } = await import('./commands/verify/index.js');
- return await cmd({ config, flags });
+ return await cmd({ astroConfig, dbConfig, flags });
+ }
+ case 'execute': {
+ 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({ config, flags });
+ return await cmd();
}
default: {
if (command == null) {
diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts
index e031108e4747..4265f36e424a 100644
--- a/packages/db/src/core/cli/migration-queries.ts
+++ b/packages/db/src/core/cli/migration-queries.ts
@@ -4,7 +4,6 @@ import * as color from 'kleur/colors';
import { customAlphabet } from 'nanoid';
import prompts from 'prompts';
import { hasPrimaryKey } from '../../runtime/index.js';
-import { isSerializedSQL } from '../../runtime/types.js';
import {
getCreateIndexQueries,
getCreateTableQuery,
@@ -12,7 +11,8 @@ import {
getReferencesConfig,
hasDefault,
schemaTypeToSqlType,
-} from '../queries.js';
+} from '../../runtime/queries.js';
+import { isSerializedSQL } from '../../runtime/types.js';
import {
type BooleanColumn,
type ColumnType,
diff --git a/packages/db/src/core/cli/migrations.ts b/packages/db/src/core/cli/migrations.ts
index 671862b61495..514d4e798a11 100644
--- a/packages/db/src/core/cli/migrations.ts
+++ b/packages/db/src/core/cli/migrations.ts
@@ -1,8 +1,8 @@
-import type { AstroConfig } from 'astro';
import deepDiff from 'deep-diff';
import { mkdir, readFile, readdir, writeFile } from 'fs/promises';
import { cyan, green, yellow } from 'kleur/colors';
-import { type DBSnapshot, tablesSchema } from '../types.js';
+import { type DBConfig, type DBSnapshot } from '../types.js';
+import { getMigrationsDirectoryUrl } from '../utils.js';
const { applyChange, diff: generateDiff } = deepDiff;
export type MigrationStatus =
@@ -24,9 +24,18 @@ export type MigrationStatus =
currentSnapshot: DBSnapshot;
};
-export async function getMigrationStatus(config: AstroConfig): Promise {
- const currentSnapshot = createCurrentSnapshot(config);
- const allMigrationFiles = await getMigrations();
+export const INITIAL_SNAPSHOT = '0000_snapshot.json';
+
+export async function getMigrationStatus({
+ dbConfig,
+ root,
+}: {
+ dbConfig: DBConfig;
+ root: URL;
+}): Promise {
+ const currentSnapshot = createCurrentSnapshot(dbConfig);
+ const dir = getMigrationsDirectoryUrl(root);
+ const allMigrationFiles = await getMigrations(dir);
if (allMigrationFiles.length === 0) {
return {
@@ -35,7 +44,7 @@ export async function getMigrationStatus(config: AstroConfig): Promise {
- const migrationFiles = await readdir('./migrations').catch((err) => {
+export async function getMigrations(dir: URL): Promise {
+ const migrationFiles = await readdir(dir).catch((err) => {
if (err.code === 'ENOENT') {
return [];
}
@@ -94,13 +103,14 @@ export async function getMigrations(): Promise {
}
export async function loadMigration(
- migration: string
+ migration: string,
+ dir: URL
): Promise<{ diff: any[]; db: string[]; confirm?: string[] }> {
- return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8'));
+ return JSON.parse(await readFile(new URL(migration, dir), 'utf-8'));
}
-export async function loadInitialSnapshot(): Promise {
- const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8'));
+export async function loadInitialSnapshot(dir: URL): Promise {
+ const snapshot = JSON.parse(await readFile(new URL(INITIAL_SNAPSHOT, dir), 'utf-8'));
// `experimentalVersion: 1` -- added the version column
if (snapshot.experimentalVersion === 1) {
return snapshot;
@@ -112,16 +122,19 @@ export async function loadInitialSnapshot(): Promise {
throw new Error('Invalid snapshot format');
}
-export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot) {
- await mkdir('./migrations', { recursive: true });
- await writeFile('./migrations/0000_snapshot.json', JSON.stringify(currentSnapshot, undefined, 2));
+export async function initializeMigrationsDirectory(currentSnapshot: DBSnapshot, dir: URL) {
+ await mkdir(dir, { recursive: true });
+ await writeFile(new URL(INITIAL_SNAPSHOT, dir), JSON.stringify(currentSnapshot, undefined, 2));
}
-export async function initializeFromMigrations(allMigrationFiles: string[]): Promise {
- const prevSnapshot = await loadInitialSnapshot();
+export async function initializeFromMigrations(
+ allMigrationFiles: string[],
+ dir: URL
+): Promise {
+ const prevSnapshot = await loadInitialSnapshot(dir);
for (const migration of allMigrationFiles) {
- if (migration === '0000_snapshot.json') continue;
- const migrationContent = await loadMigration(migration);
+ if (migration === INITIAL_SNAPSHOT) continue;
+ const migrationContent = await loadMigration(migration, dir);
migrationContent.diff.forEach((change: any) => {
applyChange(prevSnapshot, {}, change);
});
@@ -129,10 +142,8 @@ export async function initializeFromMigrations(allMigrationFiles: string[]): Pro
return prevSnapshot;
}
-export function createCurrentSnapshot(config: AstroConfig): DBSnapshot {
- // Parse to resolve non-serializable types like () => references
- const tablesConfig = tablesSchema.parse(config.db?.tables ?? {});
- const schema = JSON.parse(JSON.stringify(tablesConfig));
+export function createCurrentSnapshot({ tables = {} }: DBConfig): DBSnapshot {
+ const schema = JSON.parse(JSON.stringify(tables));
return { experimentalVersion: 1, schema };
}
export function createEmptySnapshot(): DBSnapshot {
diff --git a/packages/db/src/core/consts.ts b/packages/db/src/core/consts.ts
index 1f6771b04f85..bf295a2dc9a8 100644
--- a/packages/db/src/core/consts.ts
+++ b/packages/db/src/core/consts.ts
@@ -6,9 +6,12 @@ export const PACKAGE_NAME = JSON.parse(
export const RUNTIME_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime`);
export const RUNTIME_DRIZZLE_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime/drizzle`);
+export const RUNTIME_CONFIG_IMPORT = JSON.stringify(`${PACKAGE_NAME}/runtime/config`);
export const DB_TYPES_FILE = 'db-types.d.ts';
export const VIRTUAL_MODULE_ID = 'astro:db';
export const DB_PATH = '.astro/content.db';
+
+export const CONFIG_FILE_NAMES = ['config.ts', 'config.js', 'config.mts', 'config.mjs'];
diff --git a/packages/db/src/core/errors.ts b/packages/db/src/core/errors.ts
index 76a66d0dc954..706553a0673e 100644
--- a/packages/db/src/core/errors.ts
+++ b/packages/db/src/core/errors.ts
@@ -10,44 +10,41 @@ export const MISSING_PROJECT_ID_ERROR = `${red('▶ Directory not linked.')}
To link this directory to an Astro Studio project, run
${cyan('astro db link')}\n`;
-export const STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR = (tableName: string) => `${red(
- `▶ Writable table ${bold(tableName)} requires Astro Studio or the ${yellow(
- 'unsafeWritable'
- )} option.`
-)}
-
- Visit ${cyan('https://astro.build/studio')} to create your account
- and set ${green('studio: true')} in your astro.config.mjs file to enable Studio.\n`;
+export const MIGRATIONS_NOT_INITIALIZED = `${yellow(
+ '▶ No migrations found!'
+)}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`;
-export const UNSAFE_WRITABLE_WARNING = `${yellow(
- 'unsafeWritable'
-)} option is enabled and you are using writable tables.
- Redeploying your app may result in wiping away your database.
- I hope you know what you are doing.\n`;
+export const MISSING_EXECUTE_PATH_ERROR = `${red(
+ '▶ No file path provided.'
+)} Provide a path by running ${cyan('astro db execute ')}\n`;
-export const STUDIO_CONFIG_MISSING_CLI_ERROR = `${red('▶ This command requires Astro Studio.')}
+export const FILE_NOT_FOUND_ERROR = (path: string) =>
+ `${red('▶ File not found:')} ${bold(path)}\n`;
- Visit ${cyan('https://astro.build/studio')} to create your account
- and set ${green('studio: true')} in your astro.config.mjs file to enable Studio.\n`;
+export const SEED_ERROR = (error: string) => {
+ return `${red(`Error while seeding database:`)}\n\n${error}`;
+};
-export const MIGRATIONS_NOT_INITIALIZED = `${yellow(
- '▶ No migrations found!'
-)}\n\n To scaffold your migrations folder, run\n ${cyan('astro db sync')}\n`;
+export const REFERENCE_DNE_ERROR = (columnName: string) => {
+ return `Column ${bold(
+ columnName
+ )} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
+};
-export const SEED_WRITABLE_IN_PROD_ERROR = (tableName: string) => {
- return `${red(
- `Writable tables should not be seeded in production with data().`
- )} You can seed ${bold(
+export const FOREIGN_KEY_DNE_ERROR = (tableName: string) => {
+ return `Table ${bold(
tableName
- )} in development mode only using the "mode" flag. See the docs for more: https://www.notion.so/astroinc/astrojs-db-README-dcf6fa10de9a4f528be56cee96e8c054?pvs=4#278aed3fc37e4cec80240d1552ff6ac5`;
+ )} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
};
-export const SEED_ERROR = (tableName: string, error: string) => {
- return `${red(`Error seeding table ${bold(tableName)}:`)}\n\n${error}`;
+export const FOREIGN_KEY_REFERENCES_LENGTH_ERROR = (tableName: string) => {
+ return `Foreign key on ${bold(
+ tableName
+ )} is misconfigured. \`columns\` and \`references\` must be the same length.`;
};
-export const SEED_EMPTY_ARRAY_ERROR = (tableName: string) => {
- // Drizzle error says "values() must be called with at least one value."
- // This is specific to db.insert(). Prettify for seed().
- return SEED_ERROR(tableName, `Empty array was passed. seed() must receive at least one value.`);
+export const FOREIGN_KEY_REFERENCES_EMPTY_ERROR = (tableName: string) => {
+ return `Foreign key on ${bold(
+ tableName
+ )} is misconfigured. \`references\` array cannot be empty.`;
};
diff --git a/packages/db/src/core/integration/index.ts b/packages/db/src/core/integration/index.ts
index d9cddfd72433..4361ddfe7f67 100644
--- a/packages/db/src/core/integration/index.ts
+++ b/packages/db/src/core/integration/index.ts
@@ -4,25 +4,29 @@ import { fileURLToPath } from 'url';
import type { AstroIntegration } from 'astro';
import { mkdir, rm, writeFile } from 'fs/promises';
import { blue, yellow } from 'kleur/colors';
-import { createLocalDatabaseClient } from '../../runtime/db-client.js';
-import { DB_PATH } from '../consts.js';
-import { STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR, UNSAFE_WRITABLE_WARNING } from '../errors.js';
-import { recreateTables, seedData } from '../queries.js';
+import { CONFIG_FILE_NAMES, DB_PATH } from '../consts.js';
+import { loadDbConfigFile } from '../load-file.js';
import { type ManagedAppToken, getManagedAppTokenOrExit } from '../tokens.js';
-import { type DBTables, astroConfigWithDbSchema } from '../types.js';
-import { type VitePlugin } from '../utils.js';
+import { type DBConfig, dbConfigSchema } from '../types.js';
+import { type VitePlugin, getDbDirectoryUrl } from '../utils.js';
import { errorMap } from './error-map.js';
import { fileURLIntegration } from './file-url.js';
import { typegen } from './typegen.js';
-import { vitePluginDb } from './vite-plugin-db.js';
+import { type LateTables, vitePluginDb } from './vite-plugin-db.js';
import { vitePluginInjectEnvTs } from './vite-plugin-inject-env-ts.js';
function astroDBIntegration(): AstroIntegration {
- let connectedToRemote = false;
+ let connectToStudio = false;
+ let configFileDependencies: string[] = [];
+ let root: URL;
let appToken: ManagedAppToken | undefined;
- let schemas = {
- tables(): DBTables {
- throw new Error('tables not found');
+ let dbConfig: DBConfig;
+
+ // Make table loading "late" to pass to plugins from `config:setup`,
+ // but load during `config:done` to wait for integrations to settle.
+ let tables: LateTables = {
+ get() {
+ throw new Error('[astro:db] INTERNAL Tables not loaded yet');
},
};
let command: 'dev' | 'build' | 'preview';
@@ -31,25 +35,28 @@ function astroDBIntegration(): AstroIntegration {
hooks: {
'astro:config:setup': async ({ updateConfig, config, command: _command, logger }) => {
command = _command;
- if (_command === 'preview') return;
+ root = config.root;
+
+ if (command === 'preview') return;
let dbPlugin: VitePlugin | undefined = undefined;
- const studio = config.db?.studio ?? false;
+ connectToStudio = command === 'build';
- if (studio && command === 'build' && process.env.ASTRO_DB_TEST_ENV !== '1') {
+ if (connectToStudio) {
appToken = await getManagedAppTokenOrExit();
- connectedToRemote = true;
dbPlugin = vitePluginDb({
- connectToStudio: true,
+ connectToStudio,
appToken: appToken.token,
- schemas,
+ tables,
root: config.root,
+ srcDir: config.srcDir,
});
} else {
dbPlugin = vitePluginDb({
connectToStudio: false,
- schemas,
+ tables,
root: config.root,
+ srcDir: config.srcDir,
});
}
@@ -60,67 +67,50 @@ function astroDBIntegration(): AstroIntegration {
},
});
},
- 'astro:config:done': async ({ config, logger }) => {
+ 'astro:config:done': async ({ config }) => {
// TODO: refine where we load tables
// @matthewp: may want to load tables by path at runtime
- const configWithDb = astroConfigWithDbSchema.parse(config, { errorMap });
- const tables = configWithDb.db?.tables ?? {};
- // Redefine getTables so our integration can grab them
- schemas.tables = () => tables;
-
- const studio = configWithDb.db?.studio ?? false;
- const unsafeWritable = Boolean(configWithDb.db?.unsafeWritable);
- const foundWritableCollection = Object.entries(tables).find(([, c]) => c.writable);
- const writableAllowed = studio || unsafeWritable;
- if (!writableAllowed && foundWritableCollection) {
- logger.error(STUDIO_CONFIG_MISSING_WRITABLE_TABLE_ERROR(foundWritableCollection[0]));
- process.exit(1);
- }
- // Using writable tables with the opt-in flag. Warn them to let them
- // know the risk.
- else if (unsafeWritable && foundWritableCollection) {
- logger.warn(UNSAFE_WRITABLE_WARNING);
- }
+ const { mod, dependencies } = await loadDbConfigFile(config.root);
+ configFileDependencies = dependencies;
+ dbConfig = dbConfigSchema.parse(mod?.default ?? {}, {
+ errorMap,
+ });
+ // TODO: resolve integrations here?
+ tables.get = () => dbConfig.tables ?? {};
- if (!connectedToRemote) {
+ if (!connectToStudio && !process.env.TEST_IN_MEMORY_DB) {
const dbUrl = new URL(DB_PATH, config.root);
if (existsSync(dbUrl)) {
await rm(dbUrl);
}
await mkdir(dirname(fileURLToPath(dbUrl)), { recursive: true });
await writeFile(dbUrl, '');
-
- using db = await createLocalDatabaseClient({
- tables,
- dbUrl: dbUrl.toString(),
- seeding: true,
- });
- await recreateTables({ db, tables });
- if (configWithDb.db?.data) {
- await seedData({
- db,
- data: configWithDb.db.data,
- logger,
- mode: command === 'dev' ? 'dev' : 'build',
- });
- }
- logger.debug('Database setup complete.');
}
- await typegen({ tables, root: config.root });
+ await typegen({ tables: tables.get() ?? {}, root: config.root });
},
'astro:server:start': async ({ logger }) => {
// Wait for the server startup to log, so that this can come afterwards.
setTimeout(() => {
logger.info(
- connectedToRemote ? 'Connected to remote database.' : 'New local database created.'
+ connectToStudio ? 'Connected to remote database.' : 'New local database created.'
);
}, 100);
},
+ 'astro:server:setup': async ({ server }) => {
+ const filesToWatch = [
+ ...CONFIG_FILE_NAMES.map((c) => new URL(c, getDbDirectoryUrl(root))),
+ ...configFileDependencies.map((c) => new URL(c, root)),
+ ];
+ server.watcher.on('all', (event, relativeEntry) => {
+ const entry = new URL(relativeEntry, root);
+ if (filesToWatch.some((f) => entry.href === f.href)) {
+ server.restart();
+ }
+ });
+ },
'astro:build:start': async ({ logger }) => {
- logger.info(
- 'database: ' + (connectedToRemote ? yellow('remote') : blue('local database.'))
- );
+ logger.info('database: ' + (connectToStudio ? yellow('remote') : blue('local database.')));
},
'astro:build:done': async ({}) => {
await appToken?.destroy();
diff --git a/packages/db/src/core/integration/typegen.ts b/packages/db/src/core/integration/typegen.ts
index 0436261e16ba..4fad1bc92189 100644
--- a/packages/db/src/core/integration/typegen.ts
+++ b/packages/db/src/core/integration/typegen.ts
@@ -28,19 +28,7 @@ ${Object.entries(tables)
function generateTableType(name: string, collection: DBTable): string {
let tableType = ` export const ${name}: import(${RUNTIME_IMPORT}).Table<
${JSON.stringify(name)},
- ${JSON.stringify(
- Object.fromEntries(
- Object.entries(collection.columns).map(([columnName, column]) => [
- columnName,
- {
- // Only select columns Drizzle needs for inference
- type: column.type,
- optional: column.schema.optional,
- default: column.schema.default,
- },
- ])
- )
- )}
+ ${JSON.stringify(collection.columns)}
>;`;
return tableType;
}
diff --git a/packages/db/src/core/integration/vite-plugin-db.ts b/packages/db/src/core/integration/vite-plugin-db.ts
index 121d0eca1d62..65a9fa6a03b6 100644
--- a/packages/db/src/core/integration/vite-plugin-db.ts
+++ b/packages/db/src/core/integration/vite-plugin-db.ts
@@ -1,70 +1,125 @@
-import { DB_PATH, RUNTIME_DRIZZLE_IMPORT, RUNTIME_IMPORT, VIRTUAL_MODULE_ID } from '../consts.js';
+import { fileURLToPath } from 'node:url';
+import { normalizePath } from 'vite';
+import { SEED_DEV_FILE_NAME } from '../../runtime/queries.js';
+import {
+ DB_PATH,
+ RUNTIME_CONFIG_IMPORT,
+ RUNTIME_DRIZZLE_IMPORT,
+ RUNTIME_IMPORT,
+ VIRTUAL_MODULE_ID,
+} from '../consts.js';
import type { DBTables } from '../types.js';
-import { type VitePlugin, getRemoteDatabaseUrl } from '../utils.js';
+import { type VitePlugin, getDbDirectoryUrl, getRemoteDatabaseUrl } from '../utils.js';
+
+const LOCAL_DB_VIRTUAL_MODULE_ID = 'astro:local';
const resolvedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID;
+const resolvedLocalDbVirtualModuleId = LOCAL_DB_VIRTUAL_MODULE_ID + '/local-db';
+const resolvedSeedVirtualModuleId = '\0' + VIRTUAL_MODULE_ID + '?shouldSeed';
-type LateSchema = {
- tables: () => DBTables;
+export type LateTables = {
+ get: () => DBTables;
};
type VitePluginDBParams =
| {
connectToStudio: false;
- schemas: LateSchema;
+ tables: LateTables;
+ srcDir: URL;
root: URL;
}
| {
connectToStudio: true;
- schemas: LateSchema;
+ tables: LateTables;
appToken: string;
+ srcDir: URL;
root: URL;
};
export function vitePluginDb(params: VitePluginDBParams): VitePlugin {
+ const srcDirPath = normalizePath(fileURLToPath(params.srcDir));
return {
name: 'astro:db',
enforce: 'pre',
- resolveId(id) {
- if (id === VIRTUAL_MODULE_ID) {
- return resolvedVirtualModuleId;
+ async resolveId(id, rawImporter) {
+ if (id === LOCAL_DB_VIRTUAL_MODULE_ID) return resolvedLocalDbVirtualModuleId;
+ if (id !== VIRTUAL_MODULE_ID) return;
+ if (params.connectToStudio) return resolvedVirtualModuleId;
+
+ const importer = rawImporter ? await this.resolve(rawImporter) : null;
+ if (!importer) return resolvedVirtualModuleId;
+
+ if (importer.id.startsWith(srcDirPath)) {
+ // Seed only if the importer is in the src directory.
+ // Otherwise, we may get recursive seed calls (ex. import from db/seed.ts).
+ return resolvedSeedVirtualModuleId;
}
+ return resolvedVirtualModuleId;
},
load(id) {
- if (id !== resolvedVirtualModuleId) return;
+ if (id === resolvedLocalDbVirtualModuleId) {
+ const dbUrl = new URL(DB_PATH, params.root);
+ return `import { createLocalDatabaseClient } from ${RUNTIME_IMPORT};
+ const dbUrl = ${JSON.stringify(dbUrl)};
+
+ export const db = createLocalDatabaseClient({ dbUrl });`;
+ }
+
+ if (id !== resolvedVirtualModuleId && id !== resolvedSeedVirtualModuleId) return;
if (params.connectToStudio) {
return getStudioVirtualModContents({
appToken: params.appToken,
- tables: params.schemas.tables(),
+ tables: params.tables.get(),
});
}
- return getVirtualModContents({
+ return getLocalVirtualModContents({
root: params.root,
- tables: params.schemas.tables(),
+ tables: params.tables.get(),
+ shouldSeed: id === resolvedSeedVirtualModuleId,
});
},
};
}
-export function getVirtualModContents({ tables, root }: { tables: DBTables; root: URL }) {
- const dbUrl = new URL(DB_PATH, root);
+export function getConfigVirtualModContents() {
+ return `export * from ${RUNTIME_CONFIG_IMPORT}`;
+}
+
+export function getLocalVirtualModContents({
+ tables,
+ shouldSeed,
+}: {
+ tables: DBTables;
+ root: URL;
+ shouldSeed: boolean;
+}) {
+ const seedFilePaths = SEED_DEV_FILE_NAME.map(
+ // Format as /db/[name].ts
+ // for Vite import.meta.glob
+ (name) => new URL(name, getDbDirectoryUrl('file:///')).pathname
+ );
+
return `
-import { collectionToTable, createLocalDatabaseClient } from ${RUNTIME_IMPORT};
-import dbUrl from ${JSON.stringify(`${dbUrl}?fileurl`)};
+import { asDrizzleTable, seedLocal } from ${RUNTIME_IMPORT};
+import { db as _db } from ${JSON.stringify(LOCAL_DB_VIRTUAL_MODULE_ID)};
-const params = ${JSON.stringify({
- tables,
- seeding: false,
- })};
-params.dbUrl = dbUrl;
+export const db = _db;
-export const db = await createLocalDatabaseClient(params);
+${
+ shouldSeed
+ ? `await seedLocal({
+ db: _db,
+ tables: ${JSON.stringify(tables)},
+ fileGlob: import.meta.glob(${JSON.stringify(seedFilePaths)}),
+})`
+ : ''
+}
export * from ${RUNTIME_DRIZZLE_IMPORT};
+export * from ${RUNTIME_CONFIG_IMPORT};
-${getStringifiedCollectionExports(tables)}
-`;
+${getStringifiedCollectionExports(tables)}`;
}
export function getStudioVirtualModContents({
@@ -75,13 +130,14 @@ export function getStudioVirtualModContents({
appToken: string;
}) {
return `
-import {collectionToTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT};
+import {asDrizzleTable, createRemoteDatabaseClient} from ${RUNTIME_IMPORT};
export const db = await createRemoteDatabaseClient(${JSON.stringify(
appToken
// Respect runtime env for user overrides in SSR
)}, import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL ?? ${JSON.stringify(getRemoteDatabaseUrl())});
export * from ${RUNTIME_DRIZZLE_IMPORT};
+export * from ${RUNTIME_CONFIG_IMPORT};
${getStringifiedCollectionExports(tables)}
`;
@@ -91,7 +147,7 @@ function getStringifiedCollectionExports(tables: DBTables) {
return Object.entries(tables)
.map(
([name, collection]) =>
- `export const ${name} = collectionToTable(${JSON.stringify(name)}, ${JSON.stringify(
+ `export const ${name} = asDrizzleTable(${JSON.stringify(name)}, ${JSON.stringify(
collection
)}, false)`
)
diff --git a/packages/db/src/core/load-file.ts b/packages/db/src/core/load-file.ts
new file mode 100644
index 000000000000..cc1b23c014b2
--- /dev/null
+++ b/packages/db/src/core/load-file.ts
@@ -0,0 +1,117 @@
+import { existsSync } from 'node:fs';
+import { unlink, writeFile } from 'node:fs/promises';
+import { fileURLToPath } from 'node:url';
+import { build as esbuild } from 'esbuild';
+import { CONFIG_FILE_NAMES, VIRTUAL_MODULE_ID } from './consts.js';
+import { getConfigVirtualModContents } from './integration/vite-plugin-db.js';
+import { getDbDirectoryUrl } from './utils.js';
+
+export async function loadDbConfigFile(
+ root: URL
+): Promise<{ mod: { default?: unknown } | undefined; dependencies: string[] }> {
+ let configFileUrl: URL | undefined;
+ for (const fileName of CONFIG_FILE_NAMES) {
+ const fileUrl = new URL(fileName, getDbDirectoryUrl(root));
+ if (existsSync(fileUrl)) {
+ configFileUrl = fileUrl;
+ }
+ }
+ if (!configFileUrl) {
+ return { mod: undefined, dependencies: [] };
+ }
+ const { code, dependencies } = await bundleFile({
+ virtualModContents: getConfigVirtualModContents(),
+ root,
+ fileUrl: configFileUrl,
+ });
+ return {
+ mod: await importBundledFile({ code, root }),
+ dependencies,
+ };
+}
+
+/**
+ * Bundle arbitrary `mjs` or `ts` file.
+ * Simplified fork from Vite's `bundleConfigFile` function.
+ *
+ * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L961
+ */
+export async function bundleFile({
+ fileUrl,
+ root,
+ virtualModContents,
+}: {
+ fileUrl: URL;
+ root: URL;
+ virtualModContents: string;
+}) {
+ const result = await esbuild({
+ absWorkingDir: process.cwd(),
+ entryPoints: [fileURLToPath(fileUrl)],
+ outfile: 'out.js',
+ packages: 'external',
+ write: false,
+ target: ['node16'],
+ platform: 'node',
+ bundle: true,
+ format: 'esm',
+ sourcemap: 'inline',
+ metafile: true,
+ define: {
+ 'import.meta.env.ASTRO_STUDIO_REMOTE_DB_URL': 'undefined',
+ },
+ plugins: [
+ {
+ name: 'resolve-astro-db',
+ setup(build) {
+ build.onResolve({ filter: /^astro:db$/ }, ({ path }) => {
+ return { path, namespace: VIRTUAL_MODULE_ID };
+ });
+ build.onLoad({ namespace: VIRTUAL_MODULE_ID, filter: /.*/ }, () => {
+ return {
+ contents: virtualModContents,
+ // Needed to resolve runtime dependencies
+ resolveDir: fileURLToPath(root),
+ };
+ });
+ },
+ },
+ ],
+ });
+
+ const file = result.outputFiles[0];
+ if (!file) {
+ throw new Error(`Unexpected: no output file`);
+ }
+
+ return {
+ code: file.text,
+ dependencies: Object.keys(result.metafile.inputs),
+ };
+}
+
+/**
+ * Forked from Vite config loader, replacing CJS-based path concat with ESM only
+ *
+ * @see https://github.com/vitejs/vite/blob/main/packages/vite/src/node/config.ts#L1074
+ */
+export async function importBundledFile({
+ code,
+ root,
+}: {
+ code: string;
+ root: URL;
+}): Promise<{ default?: unknown }> {
+ // Write it to disk, load it with native Node ESM, then delete the file.
+ const tmpFileUrl = new URL(`./db.timestamp-${Date.now()}.mjs`, root);
+ await writeFile(tmpFileUrl, code, { encoding: 'utf8' });
+ try {
+ return await import(/* @vite-ignore */ tmpFileUrl.pathname);
+ } finally {
+ try {
+ await unlink(tmpFileUrl);
+ } catch {
+ // already removed if this function is called twice simultaneously
+ }
+ }
+}
diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts
deleted file mode 100644
index f699a297a3bf..000000000000
--- a/packages/db/src/core/queries.ts
+++ /dev/null
@@ -1,292 +0,0 @@
-import type { AstroIntegrationLogger } from 'astro';
-import { type SQL, getTableName, sql } from 'drizzle-orm';
-import { SQLiteAsyncDialect, type SQLiteInsert } from 'drizzle-orm/sqlite-core';
-import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
-import { bold } from 'kleur/colors';
-import {
- type BooleanColumn,
- type ColumnType,
- type DBColumn,
- type DBTable,
- type DBTables,
- type DateColumn,
- type JsonColumn,
- type NumberColumn,
- type TextColumn,
-} from '../core/types.js';
-import type {
- ColumnsConfig,
- DBUserConfig,
- MaybeArray,
- ResolvedCollectionConfig,
-} from '../core/types.js';
-import { hasPrimaryKey } from '../runtime/index.js';
-import { isSerializedSQL } from '../runtime/types.js';
-import { SEED_EMPTY_ARRAY_ERROR, SEED_ERROR, SEED_WRITABLE_IN_PROD_ERROR } from './errors.js';
-
-const sqlite = new SQLiteAsyncDialect();
-
-export async function recreateTables({
- db,
- tables,
-}: {
- db: SqliteRemoteDatabase;
- tables: DBTables;
-}) {
- const setupQueries: SQL[] = [];
- for (const [name, collection] of Object.entries(tables)) {
- const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
- const createQuery = sql.raw(getCreateTableQuery(name, collection));
- const indexQueries = getCreateIndexQueries(name, collection);
- setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
- }
- for (const q of setupQueries) {
- await db.run(q);
- }
-}
-
-export async function seedData({
- db,
- data,
- logger,
- mode,
-}: {
- db: SqliteRemoteDatabase;
- data: DBUserConfig['data'];
- logger?: AstroIntegrationLogger;
- mode: 'dev' | 'build';
-}) {
- const dataFns = Array.isArray(data) ? data : [data];
- try {
- for (const dataFn of dataFns) {
- await dataFn({
- seed: async (config, values) => {
- seedErrorChecks(mode, config, values);
- try {
- await db.insert(config.table).values(values as any);
- } catch (e) {
- const msg = e instanceof Error ? e.message : String(e);
- throw new Error(SEED_ERROR(getTableName(config.table), msg));
- }
- },
- seedReturning: async (config, values) => {
- seedErrorChecks(mode, config, values);
- try {
- let result: SQLiteInsert = db
- .insert(config.table)
- .values(values as any)
- .returning();
- if (!Array.isArray(values)) {
- result = result.get();
- }
- return result;
- } catch (e) {
- const msg = e instanceof Error ? e.message : String(e);
- throw new Error(SEED_ERROR(getTableName(config.table), msg));
- }
- },
- db,
- mode,
- });
- }
- } catch (e) {
- if (!(e instanceof Error)) throw e;
- (logger ?? console).error(e.message);
- }
-}
-
-function seedErrorChecks(
- mode: 'dev' | 'build',
- { table, writable }: ResolvedCollectionConfig,
- values: MaybeArray
-) {
- const tableName = getTableName(table);
- if (writable && mode === 'build' && process.env.ASTRO_DB_TEST_ENV !== '1') {
- throw new Error(SEED_WRITABLE_IN_PROD_ERROR(tableName));
- }
- if (Array.isArray(values) && values.length === 0) {
- throw new Error(SEED_EMPTY_ARRAY_ERROR(tableName));
- }
-}
-
-export function getCreateTableQuery(collectionName: string, collection: DBTable) {
- let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
-
- const colQueries = [];
- const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) =>
- hasPrimaryKey(column)
- );
- if (!colHasPrimaryKey) {
- colQueries.push('_id INTEGER PRIMARY KEY');
- }
- for (const [columnName, column] of Object.entries(collection.columns)) {
- const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
- column.type
- )}${getModifiers(columnName, column)}`;
- colQueries.push(colQuery);
- }
-
- colQueries.push(...getCreateForeignKeyQueries(collectionName, collection));
-
- query += colQueries.join(', ') + ')';
- return query;
-}
-
-export function getCreateIndexQueries(
- collectionName: string,
- collection: Pick
-) {
- let queries: string[] = [];
- for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) {
- const onColNames = asArray(indexProps.on);
- const onCols = onColNames.map((colName) => sqlite.escapeName(colName));
-
- const unique = indexProps.unique ? 'UNIQUE ' : '';
- const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName(
- indexName
- )} ON ${sqlite.escapeName(collectionName)} (${onCols.join(', ')})`;
- queries.push(indexQuery);
- }
- return queries;
-}
-
-export function getCreateForeignKeyQueries(collectionName: string, collection: DBTable) {
- let queries: string[] = [];
- for (const foreignKey of collection.foreignKeys ?? []) {
- const columns = asArray(foreignKey.columns);
- const references = asArray(foreignKey.references);
-
- if (columns.length !== references.length) {
- throw new Error(
- `Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.`
- );
- }
- const referencedCollection = references[0]?.schema.collection;
- if (!referencedCollection) {
- throw new Error(
- `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.`
- );
- }
- const query = `FOREIGN KEY (${columns
- .map((f) => sqlite.escapeName(f))
- .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references
- .map((r) => sqlite.escapeName(r.schema.name!))
- .join(', ')})`;
- queries.push(query);
- }
- return queries;
-}
-
-function asArray(value: T | T[]) {
- return Array.isArray(value) ? value : [value];
-}
-
-export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' {
- switch (type) {
- case 'date':
- case 'text':
- case 'json':
- return 'text';
- case 'number':
- case 'boolean':
- return 'integer';
- }
-}
-
-export function getModifiers(columnName: string, column: DBColumn) {
- let modifiers = '';
- if (hasPrimaryKey(column)) {
- return ' PRIMARY KEY';
- }
- if (!column.schema.optional) {
- modifiers += ' NOT NULL';
- }
- if (column.schema.unique) {
- modifiers += ' UNIQUE';
- }
- if (hasDefault(column)) {
- modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
- }
- const references = getReferencesConfig(column);
- if (references) {
- const { collection, name } = references.schema;
- if (!collection || !name) {
- throw new Error(
- `Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`tables\` object in your Astro config?`
- );
- }
-
- modifiers += ` REFERENCES ${sqlite.escapeName(collection)} (${sqlite.escapeName(name)})`;
- }
- return modifiers;
-}
-
-export function getReferencesConfig(column: DBColumn) {
- const canHaveReferences = column.type === 'number' || column.type === 'text';
- if (!canHaveReferences) return undefined;
- return column.schema.references;
-}
-
-// Using `DBColumn` will not narrow `default` based on the column `type`
-// Handle each column separately
-type WithDefaultDefined = T & {
- schema: Required>;
-};
-type DBColumnWithDefault =
- | WithDefaultDefined
- | WithDefaultDefined
- | WithDefaultDefined
- | WithDefaultDefined
- | WithDefaultDefined;
-
-// Type narrowing the default fails on union types, so use a type guard
-export function hasDefault(column: DBColumn): column is DBColumnWithDefault {
- if (column.schema.default !== undefined) {
- return true;
- }
- if (hasPrimaryKey(column) && column.type === 'number') {
- return true;
- }
- return false;
-}
-
-function toDefault(def: T | SQL): string {
- const type = typeof def;
- if (type === 'string') {
- return sqlite.escapeString(def as string);
- } else if (type === 'boolean') {
- return def ? 'TRUE' : 'FALSE';
- } else {
- return def + '';
- }
-}
-
-function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string {
- if (isSerializedSQL(column.schema.default)) {
- return column.schema.default.sql;
- }
-
- switch (column.type) {
- case 'boolean':
- case 'number':
- case 'text':
- case 'date':
- return toDefault(column.schema.default);
- case 'json': {
- let stringified = '';
- try {
- stringified = JSON.stringify(column.schema.default);
- } catch (e) {
- // eslint-disable-next-line no-console
- console.log(
- `Invalid default value for column ${bold(
- columnName
- )}. Defaults must be valid JSON when using the \`json()\` type.`
- );
- process.exit(0);
- }
-
- return sqlite.escapeString(stringified);
- }
- }
-}
diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts
index a4896a8bead6..f9de8f8ee519 100644
--- a/packages/db/src/core/types.ts
+++ b/packages/db/src/core/types.ts
@@ -1,8 +1,6 @@
-import type { InferSelectModel } from 'drizzle-orm';
import { SQL } from 'drizzle-orm';
-import { SQLiteAsyncDialect, type SQLiteInsertValue } from 'drizzle-orm/sqlite-core';
+import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { type ZodTypeDef, z } from 'zod';
-import { type SqliteDB, type Table, collectionToTable } from '../runtime/index.js';
import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js';
import { errorMap } from './integration/error-map.js';
@@ -26,6 +24,7 @@ const baseColumnSchema = z.object({
// Defined when `defineReadableTable()` is called
name: z.string().optional(),
+ // TODO: rename to `tableName`. Breaking schema change
collection: z.string().optional(),
});
@@ -181,41 +180,26 @@ const foreignKeysSchema: z.ZodType>;
-const baseCollectionSchema = z.object({
+export const tableSchema = z.object({
columns: columnsSchema,
indexes: z.record(indexSchema).optional(),
foreignKeys: z.array(foreignKeysSchema).optional(),
});
-export const readableCollectionSchema = baseCollectionSchema.extend({
- writable: z.literal(false),
-});
-
-export const writableCollectionSchema = baseCollectionSchema.extend({
- writable: z.literal(true),
-});
-
-export const collectionSchema = z.union([readableCollectionSchema, writableCollectionSchema]);
-export const tablesSchema = z.preprocess((rawCollections) => {
+export const tablesSchema = z.preprocess((rawTables) => {
// Use `z.any()` to avoid breaking object references
- const tables = z.record(z.any()).parse(rawCollections, { errorMap });
- for (const [collectionName, collection] of Object.entries(tables)) {
- // Append `table` object for data seeding.
- // Must append at runtime so table name exists.
- collection.table = collectionToTable(
- collectionName,
- collectionSchema.parse(collection, { errorMap })
- );
- // Append collection and column names to columns.
- // Used to track collection info for references.
- const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap });
+ const tables = z.record(z.any()).parse(rawTables, { errorMap });
+ for (const [tableName, table] of Object.entries(tables)) {
+ // Append table and column names to columns.
+ // Used to track table info for references.
+ const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap });
for (const [columnName, column] of Object.entries(columns)) {
column.schema.name = columnName;
- column.schema.collection = collectionName;
+ column.schema.collection = tableName;
}
}
- return rawCollections;
-}, z.record(collectionSchema));
+ return rawTables;
+}, z.record(tableSchema));
export type BooleanColumn = z.infer;
export type BooleanColumnInput = z.input;
@@ -243,7 +227,7 @@ export type DBColumnInput =
| TextColumnInput
| JsonColumnInput;
export type DBColumns = z.infer;
-export type DBTable = z.infer;
+export type DBTable = z.infer;
export type DBTables = Record;
export type DBSnapshot = {
schema: Record;
@@ -253,62 +237,24 @@ export type DBSnapshot = {
*/
experimentalVersion: number;
};
-export type ReadableDBTable = z.infer;
-export type WritableDBTable = z.infer;
-
-export type DBDataContext = {
- db: SqliteDB;
- seed: (
- collection: ResolvedCollectionConfig,
- data: MaybeArray>>
- ) => Promise;
- seedReturning: <
- TColumns extends ColumnsConfig,
- TData extends MaybeArray>>,
- >(
- collection: ResolvedCollectionConfig,
- data: TData
- ) => Promise<
- TData extends Array>>
- ? InferSelectModel>[]
- : InferSelectModel>
- >;
- mode: 'dev' | 'build';
-};
-
-export function defineData(fn: (ctx: DBDataContext) => MaybePromise) {
- return fn;
-}
-
-const dbDataFn = z.function().returns(z.union([z.void(), z.promise(z.void())]));
export const dbConfigSchema = z.object({
- studio: z.boolean().optional(),
tables: tablesSchema.optional(),
- data: z.union([dbDataFn, z.array(dbDataFn)]).optional(),
- unsafeWritable: z.boolean().optional().default(false),
});
-type DataFunction = (params: DBDataContext) => MaybePromise;
-
-export type DBUserConfig = Omit, 'data'> & {
- data: DataFunction | DataFunction[];
-};
+export type DBConfigInput = z.input;
+export type DBConfig = z.infer;
-export const astroConfigWithDbSchema = z.object({
- db: dbConfigSchema.optional(),
-});
+export type ColumnsConfig = z.input['columns'];
+export type OutputColumnsConfig = z.output['columns'];
-export type ColumnsConfig = z.input['columns'];
-
-interface CollectionConfig
+export interface TableConfig
// use `extends` to ensure types line up with zod,
// only adding generics for type completions.
- extends Pick, 'columns' | 'indexes' | 'foreignKeys'> {
+ extends Pick, 'columns' | 'indexes' | 'foreignKeys'> {
columns: TColumns;
foreignKeys?: Array<{
columns: MaybeArray>;
- // TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here...
references: () => MaybeArray>;
}>;
indexes?: Record>;
@@ -318,69 +264,11 @@ interface IndexConfig extends z.input>;
}
-export type ResolvedCollectionConfig<
- TColumns extends ColumnsConfig = ColumnsConfig,
- Writable extends boolean = boolean,
-> = CollectionConfig & {
- writable: Writable;
- table: Table;
-};
-
-function baseDefineCollection(
- userConfig: CollectionConfig,
- writable: TWritable
-): ResolvedCollectionConfig {
- return {
- ...userConfig,
- writable,
- // set at runtime to get the table name
- table: null!,
- };
-}
-
-export function defineReadableTable(
- userConfig: CollectionConfig
-): ResolvedCollectionConfig {
- return baseDefineCollection(userConfig, false);
-}
-
-export function defineWritableTable(
- userConfig: CollectionConfig
-): ResolvedCollectionConfig {
- return baseDefineCollection(userConfig, true);
-}
-
-export type AstroConfigWithDB = z.input;
+/** @deprecated Use `TableConfig` instead */
+export type ResolvedCollectionConfig =
+ TableConfig;
// We cannot use `Omit`,
// since Omit collapses our union type on primary key.
-type NumberColumnOpts = z.input;
-type TextColumnOpts = z.input;
-
-function createColumn>(type: S, schema: T) {
- return {
- type,
- /**
- * @internal
- */
- schema,
- };
-}
-
-export const column = {
- number: (opts: T = {} as T) => {
- return createColumn('number', opts) satisfies { type: 'number' };
- },
- boolean: (opts: T = {} as T) => {
- return createColumn('boolean', opts) satisfies { type: 'boolean' };
- },
- text: (opts: T = {} as T) => {
- return createColumn('text', opts) satisfies { type: 'text' };
- },
- date(opts: T = {} as T) {
- return createColumn('date', opts) satisfies { type: 'date' };
- },
- json(opts: T = {} as T) {
- return createColumn('json', opts) satisfies { type: 'json' };
- },
-};
+export type NumberColumnOpts = z.input;
+export type TextColumnOpts = z.input;
diff --git a/packages/db/src/core/utils.ts b/packages/db/src/core/utils.ts
index b579770400bc..674514711075 100644
--- a/packages/db/src/core/utils.ts
+++ b/packages/db/src/core/utils.ts
@@ -17,3 +17,11 @@ export function getAstroStudioUrl(): string {
const env = getAstroStudioEnv();
return env.ASTRO_STUDIO_URL || 'https://stardate.astro.build';
}
+
+export function getDbDirectoryUrl(root: URL | string) {
+ return new URL('db/', root);
+}
+
+export function getMigrationsDirectoryUrl(root: URL | string) {
+ return new URL('migrations/', getDbDirectoryUrl(root));
+}
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
index f280f35587b7..0073f2b33c16 100644
--- a/packages/db/src/index.ts
+++ b/packages/db/src/index.ts
@@ -1,5 +1,4 @@
-export { defineReadableTable, defineWritableTable, defineData, column } from './core/types.js';
-export type { ResolvedCollectionConfig, DBDataContext } from './core/types.js';
+export type { ResolvedCollectionConfig, TableConfig } from './core/types.js';
export { cli } from './core/cli/index.js';
export { integration as default } from './core/integration/index.js';
-export { sql, NOW, TRUE, FALSE } from './runtime/index.js';
+export { sql, NOW, TRUE, FALSE, defineDB, defineTable, column } from './runtime/config.js';
diff --git a/packages/db/src/runtime/config.ts b/packages/db/src/runtime/config.ts
new file mode 100644
index 000000000000..5fb87e7a5c15
--- /dev/null
+++ b/packages/db/src/runtime/config.ts
@@ -0,0 +1,48 @@
+import type {
+ BooleanColumnInput,
+ ColumnsConfig,
+ DBConfigInput,
+ DateColumnInput,
+ JsonColumnInput,
+ NumberColumnOpts,
+ TableConfig,
+ TextColumnOpts,
+} from '../core/types.js';
+
+function createColumn>(type: S, schema: T) {
+ return {
+ type,
+ /**
+ * @internal
+ */
+ schema,
+ };
+}
+
+export const column = {
+ number: (opts: T = {} as T) => {
+ return createColumn('number', opts) satisfies { type: 'number' };
+ },
+ boolean: (opts: T = {} as T) => {
+ return createColumn('boolean', opts) satisfies { type: 'boolean' };
+ },
+ text: (opts: T = {} as T) => {
+ return createColumn('text', opts) satisfies { type: 'text' };
+ },
+ date(opts: T = {} as T) {
+ return createColumn('date', opts) satisfies { type: 'date' };
+ },
+ json(opts: T = {} as T) {
+ return createColumn('json', opts) satisfies { type: 'json' };
+ },
+};
+
+export function defineTable(userConfig: TableConfig) {
+ return userConfig;
+}
+
+export function defineDB(userConfig: DBConfigInput) {
+ return userConfig;
+}
+
+export { sql, NOW, TRUE, FALSE } from './index.js';
diff --git a/packages/db/src/runtime/db-client.ts b/packages/db/src/runtime/db-client.ts
index 9058206760af..a16a7b7c0540 100644
--- a/packages/db/src/runtime/db-client.ts
+++ b/packages/db/src/runtime/db-client.ts
@@ -1,59 +1,19 @@
import type { InStatement } from '@libsql/client';
import { createClient } from '@libsql/client';
-import { getTableName } from 'drizzle-orm';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { drizzle as drizzleLibsql } from 'drizzle-orm/libsql';
-import { type SQLiteTable } from 'drizzle-orm/sqlite-core';
import { drizzle as drizzleProxy } from 'drizzle-orm/sqlite-proxy';
import { z } from 'zod';
-import { type DBTables } from '../core/types.js';
const isWebContainer = !!process.versions?.webcontainer;
-interface LocalDatabaseClient extends LibSQLDatabase, Disposable {}
-
-export async function createLocalDatabaseClient({
- tables,
- dbUrl,
- seeding,
-}: {
- dbUrl: string;
- tables: DBTables;
- seeding: boolean;
-}): Promise {
+export function createLocalDatabaseClient({ dbUrl }: { dbUrl: string }): LibSQLDatabase {
const url = isWebContainer ? 'file:content.db' : dbUrl;
- const client = createClient({ url });
- const db = Object.assign(drizzleLibsql(client), {
- [Symbol.dispose || Symbol.for('Symbol.dispose')]() {
- client.close();
- },
- });
-
- if (seeding) return db;
-
- const { insert: drizzleInsert, update: drizzleUpdate, delete: drizzleDelete } = db;
- return Object.assign(db, {
- insert(Table: SQLiteTable) {
- checkIfModificationIsAllowed(tables, Table);
- return drizzleInsert.call(this, Table);
- },
- update(Table: SQLiteTable) {
- checkIfModificationIsAllowed(tables, Table);
- return drizzleUpdate.call(this, Table);
- },
- delete(Table: SQLiteTable) {
- checkIfModificationIsAllowed(tables, Table);
- return drizzleDelete.call(this, Table);
- },
- });
-}
+ console.log('memory', process.env.TEST_IN_MEMORY_DB);
+ const client = createClient({ url: process.env.TEST_IN_MEMORY_DB ? ':memory:' : url });
+ const db = drizzleLibsql(client);
-function checkIfModificationIsAllowed(tables: DBTables, Table: SQLiteTable) {
- const tableName = getTableName(Table);
- const collection = tables[tableName];
- if (!collection.writable) {
- throw new Error(`The [${tableName}] collection is read-only.`);
- }
+ return db;
}
export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string) {
@@ -61,8 +21,6 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
const db = drizzleProxy(async (sql, parameters, method) => {
const requestBody: InStatement = { sql, args: parameters };
- // eslint-disable-next-line no-console
- console.info(JSON.stringify(requestBody));
const res = await fetch(url, {
method: 'POST',
headers: {
@@ -107,5 +65,9 @@ export function createRemoteDatabaseClient(appToken: string, remoteDbURL: string
return { rows: rowValues };
});
+
+ (db as any).batch = (_drizzleQueries: Array>) => {
+ throw new Error('db.batch() is not currently supported.');
+ };
return db;
}
diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts
index 7ab5264cdca3..501ae7a22233 100644
--- a/packages/db/src/runtime/index.ts
+++ b/packages/db/src/runtime/index.ts
@@ -1,4 +1,5 @@
import { type ColumnBuilderBaseConfig, type ColumnDataType, sql } from 'drizzle-orm';
+import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import {
type IndexBuilder,
type SQLiteColumnBuilderBase,
@@ -8,14 +9,14 @@ import {
sqliteTable,
text,
} from 'drizzle-orm/sqlite-core';
-import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy';
import { type DBColumn, type DBTable } from '../core/types.js';
import { type SerializedSQL, isSerializedSQL } from './types.js';
export { sql };
-export type SqliteDB = SqliteRemoteDatabase;
+export type SqliteDB = LibSQLDatabase;
export type { Table } from './types.js';
export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js';
+export { seedLocal } from './queries.js';
export function hasPrimaryKey(column: DBColumn) {
return 'primaryKey' in column.schema && !!column.schema.primaryKey;
@@ -54,17 +55,17 @@ type D1ColumnBuilder = SQLiteColumnBuilderBase<
ColumnBuilderBaseConfig & { data: unknown }
>;
-export function collectionToTable(name: string, collection: DBTable) {
+export function asDrizzleTable(name: string, table: DBTable) {
const columns: Record = {};
- if (!Object.entries(collection.columns).some(([, column]) => hasPrimaryKey(column))) {
+ if (!Object.entries(table.columns).some(([, column]) => hasPrimaryKey(column))) {
columns['_id'] = integer('_id').primaryKey();
}
- for (const [columnName, column] of Object.entries(collection.columns)) {
+ for (const [columnName, column] of Object.entries(table.columns)) {
columns[columnName] = columnMapper(columnName, column);
}
- const table = sqliteTable(name, columns, (ormTable) => {
+ const drizzleTable = sqliteTable(name, columns, (ormTable) => {
const indexes: Record = {};
- for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) {
+ for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) {
const onColNames = Array.isArray(indexProps.on) ? indexProps.on : [indexProps.on];
const onCols = onColNames.map((colName) => ormTable[colName]);
if (!atLeastOne(onCols)) continue;
@@ -73,7 +74,7 @@ export function collectionToTable(name: string, collection: DBTable) {
}
return indexes;
});
- return table;
+ return drizzleTable;
}
function atLeastOne(arr: T[]): arr is [T, ...T[]] {
diff --git a/packages/db/src/runtime/queries.ts b/packages/db/src/runtime/queries.ts
new file mode 100644
index 000000000000..4341fd1237ab
--- /dev/null
+++ b/packages/db/src/runtime/queries.ts
@@ -0,0 +1,244 @@
+import { LibsqlError } from '@libsql/client';
+import { type SQL, sql } from 'drizzle-orm';
+import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
+import { bold } from 'kleur/colors';
+import {
+ FOREIGN_KEY_DNE_ERROR,
+ FOREIGN_KEY_REFERENCES_EMPTY_ERROR,
+ FOREIGN_KEY_REFERENCES_LENGTH_ERROR,
+ REFERENCE_DNE_ERROR,
+ SEED_ERROR,
+} from '../core/errors.js';
+import type {
+ BooleanColumn,
+ ColumnType,
+ DBColumn,
+ DBTable,
+ DBTables,
+ DateColumn,
+ JsonColumn,
+ NumberColumn,
+ TextColumn,
+} from '../core/types.js';
+import { type SqliteDB, hasPrimaryKey } from './index.js';
+import { isSerializedSQL } from './types.js';
+
+const sqlite = new SQLiteAsyncDialect();
+
+export const SEED_DEV_FILE_NAME = ['seed.ts', 'seed.js', 'seed.mjs', 'seed.mts'];
+
+export async function seedLocal({
+ db,
+ tables,
+ // Glob all potential seed files to catch renames and deletions.
+ fileGlob,
+}: {
+ db: SqliteDB;
+ tables: DBTables;
+ fileGlob: Record Promise>;
+}) {
+ await recreateTables({ db, tables });
+ for (const fileName of SEED_DEV_FILE_NAME) {
+ const key = Object.keys(fileGlob).find((f) => f.endsWith(fileName));
+ if (key) {
+ await fileGlob[key]().catch((e) => {
+ if (e instanceof LibsqlError) {
+ throw new Error(SEED_ERROR(e.message));
+ }
+ throw e;
+ });
+ return;
+ }
+ }
+}
+
+export async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) {
+ const setupQueries: SQL[] = [];
+ for (const [name, table] of Object.entries(tables)) {
+ const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
+ const createQuery = sql.raw(getCreateTableQuery(name, table));
+ const indexQueries = getCreateIndexQueries(name, table);
+ setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
+ }
+ await db.batch([
+ db.run(sql`pragma defer_foreign_keys=true;`),
+ ...setupQueries.map((q) => db.run(q)),
+ ]);
+}
+
+export function getCreateTableQuery(tableName: string, table: DBTable) {
+ let query = `CREATE TABLE ${sqlite.escapeName(tableName)} (`;
+
+ const colQueries = [];
+ const colHasPrimaryKey = Object.entries(table.columns).find(([, column]) =>
+ hasPrimaryKey(column)
+ );
+ if (!colHasPrimaryKey) {
+ colQueries.push('_id INTEGER PRIMARY KEY');
+ }
+ for (const [columnName, column] of Object.entries(table.columns)) {
+ const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
+ column.type
+ )}${getModifiers(columnName, column)}`;
+ colQueries.push(colQuery);
+ }
+
+ colQueries.push(...getCreateForeignKeyQueries(tableName, table));
+
+ query += colQueries.join(', ') + ')';
+ return query;
+}
+
+export function getCreateIndexQueries(tableName: string, table: Pick) {
+ let queries: string[] = [];
+ for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) {
+ const onColNames = asArray(indexProps.on);
+ const onCols = onColNames.map((colName) => sqlite.escapeName(colName));
+
+ const unique = indexProps.unique ? 'UNIQUE ' : '';
+ const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName(
+ indexName
+ )} ON ${sqlite.escapeName(tableName)} (${onCols.join(', ')})`;
+ queries.push(indexQuery);
+ }
+ return queries;
+}
+
+export function getCreateForeignKeyQueries(tableName: string, table: DBTable) {
+ let queries: string[] = [];
+ for (const foreignKey of table.foreignKeys ?? []) {
+ const columns = asArray(foreignKey.columns);
+ const references = asArray(foreignKey.references);
+
+ if (columns.length !== references.length) {
+ throw new Error(FOREIGN_KEY_REFERENCES_LENGTH_ERROR(tableName));
+ }
+ const firstReference = references[0];
+ if (!firstReference) {
+ throw new Error(FOREIGN_KEY_REFERENCES_EMPTY_ERROR(tableName));
+ }
+ const referencedTable = firstReference.schema.collection;
+ if (!referencedTable) {
+ throw new Error(FOREIGN_KEY_DNE_ERROR(tableName));
+ }
+ const query = `FOREIGN KEY (${columns
+ .map((f) => sqlite.escapeName(f))
+ .join(', ')}) REFERENCES ${sqlite.escapeName(referencedTable)}(${references
+ .map((r) => sqlite.escapeName(r.schema.name!))
+ .join(', ')})`;
+ queries.push(query);
+ }
+ return queries;
+}
+
+function asArray(value: T | T[]) {
+ return Array.isArray(value) ? value : [value];
+}
+
+export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' {
+ switch (type) {
+ case 'date':
+ case 'text':
+ case 'json':
+ return 'text';
+ case 'number':
+ case 'boolean':
+ return 'integer';
+ }
+}
+
+export function getModifiers(columnName: string, column: DBColumn) {
+ let modifiers = '';
+ if (hasPrimaryKey(column)) {
+ return ' PRIMARY KEY';
+ }
+ if (!column.schema.optional) {
+ modifiers += ' NOT NULL';
+ }
+ if (column.schema.unique) {
+ modifiers += ' UNIQUE';
+ }
+ if (hasDefault(column)) {
+ modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`;
+ }
+ const references = getReferencesConfig(column);
+ if (references) {
+ const { collection: tableName, name } = references.schema;
+ if (!tableName || !name) {
+ throw new Error(REFERENCE_DNE_ERROR(columnName));
+ }
+
+ modifiers += ` REFERENCES ${sqlite.escapeName(tableName)} (${sqlite.escapeName(name)})`;
+ }
+ return modifiers;
+}
+
+export function getReferencesConfig(column: DBColumn) {
+ const canHaveReferences = column.type === 'number' || column.type === 'text';
+ if (!canHaveReferences) return undefined;
+ return column.schema.references;
+}
+
+// Using `DBColumn` will not narrow `default` based on the column `type`
+// Handle each column separately
+type WithDefaultDefined = T & {
+ schema: Required>;
+};
+type DBColumnWithDefault =
+ | WithDefaultDefined
+ | WithDefaultDefined
+ | WithDefaultDefined
+ | WithDefaultDefined
+ | WithDefaultDefined;
+
+// Type narrowing the default fails on union types, so use a type guard
+export function hasDefault(column: DBColumn): column is DBColumnWithDefault {
+ if (column.schema.default !== undefined) {
+ return true;
+ }
+ if (hasPrimaryKey(column) && column.type === 'number') {
+ return true;
+ }
+ return false;
+}
+
+function toDefault(def: T | SQL): string {
+ const type = typeof def;
+ if (type === 'string') {
+ return sqlite.escapeString(def as string);
+ } else if (type === 'boolean') {
+ return def ? 'TRUE' : 'FALSE';
+ } else {
+ return def + '';
+ }
+}
+
+function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string {
+ if (isSerializedSQL(column.schema.default)) {
+ return column.schema.default.sql;
+ }
+
+ switch (column.type) {
+ case 'boolean':
+ case 'number':
+ case 'text':
+ case 'date':
+ return toDefault(column.schema.default);
+ case 'json': {
+ let stringified = '';
+ try {
+ stringified = JSON.stringify(column.schema.default);
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.log(
+ `Invalid default value for column ${bold(
+ columnName
+ )}. Defaults must be valid JSON when using the \`json()\` type.`
+ );
+ process.exit(0);
+ }
+
+ return sqlite.escapeString(stringified);
+ }
+ }
+}
diff --git a/packages/db/src/runtime/types.ts b/packages/db/src/runtime/types.ts
index 9bc35bda61ac..08ab16a0cc71 100644
--- a/packages/db/src/runtime/types.ts
+++ b/packages/db/src/runtime/types.ts
@@ -1,6 +1,6 @@
import type { ColumnBaseConfig, ColumnDataType } from 'drizzle-orm';
import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core';
-import type { ColumnsConfig, DBColumn } from '../core/types.js';
+import type { ColumnsConfig, DBColumn, OutputColumnsConfig } from '../core/types.js';
type GeneratedConfig = Pick<
ColumnBaseConfig,
@@ -76,7 +76,7 @@ export type Column = T ex
export type Table<
TTableName extends string,
- TColumns extends ColumnsConfig,
+ TColumns extends OutputColumnsConfig | ColumnsConfig,
> = SQLiteTableWithColumns<{
name: TTableName;
schema: undefined;
diff --git a/packages/db/src/utils.ts b/packages/db/src/utils.ts
new file mode 100644
index 000000000000..0b4c31832f06
--- /dev/null
+++ b/packages/db/src/utils.ts
@@ -0,0 +1 @@
+export { asDrizzleTable } from './runtime/index.js';
diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js
index 8a8d9caea2e6..19c105532b12 100644
--- a/packages/db/test/basics.test.js
+++ b/packages/db/test/basics.test.js
@@ -3,10 +3,6 @@ import { load as cheerioLoad } from 'cheerio';
import testAdapter from '../../astro/test/test-adapter.js';
import { loadFixture } from '../../astro/test/test-utils.js';
-// TODO(fks): Rename this to something more generic/generally useful
-// like `ASTRO_MONOREPO_TEST_ENV` if @astrojs/db is merged into astro.
-process.env.ASTRO_DB_TEST_ENV = '1';
-
describe('astro:db', () => {
let fixture;
before(async () => {
@@ -17,16 +13,25 @@ describe('astro:db', () => {
});
});
- describe('production', () => {
+ // Note(bholmesdev): Use in-memory db to avoid
+ // Multiple dev servers trying to unlink and remount
+ // the same database file.
+ process.env.TEST_IN_MEMORY_DB = 'true';
+ describe('development', () => {
+ let devServer;
+
before(async () => {
- await fixture.build();
+ console.log('starting dev server');
+ devServer = await fixture.startDevServer();
+ });
+
+ after(async () => {
+ await devServer.stop();
+ process.env.TEST_IN_MEMORY_DB = undefined;
});
it('Prints the list of authors', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/');
- const res = await app.render(request);
- const html = await res.text();
+ const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);
const ul = $('.authors-list');
@@ -34,71 +39,36 @@ describe('astro:db', () => {
expect(ul.children().eq(0).text()).to.equal('Ben');
});
- it('Errors when inserting to a readonly collection', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/insert-into-readonly');
- const res = await app.render(request);
- const html = await res.text();
+ it('Allows expression defaults for date columns', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);
- expect($('#error').text()).to.equal('The [Author] collection is read-only.');
+ const themeAdded = $($('.themes-list .theme-added')[0]).text();
+ expect(new Date(themeAdded).getTime()).to.not.be.NaN;
});
- it('Does not error when inserting into writable collection', async () => {
- const app = await fixture.loadTestAdapterApp();
- const request = new Request('http://example.com/insert-into-writable');
- const res = await app.render(request);
- const html = await res.text();
+ it('Defaults can be overridden for dates', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
const $ = cheerioLoad(html);
- expect($('#error').text()).to.equal('');
+ const themeAdded = $($('.themes-list .theme-added')[1]).text();
+ expect(new Date(themeAdded).getTime()).to.not.be.NaN;
});
- describe('Expression defaults', () => {
- let app;
- before(async () => {
- app = await fixture.loadTestAdapterApp();
- });
-
- it('Allows expression defaults for date columns', async () => {
- const request = new Request('http://example.com/');
- const res = await app.render(request);
- const html = await res.text();
- const $ = cheerioLoad(html);
-
- const themeAdded = $($('.themes-list .theme-added')[0]).text();
- expect(new Date(themeAdded).getTime()).to.not.be.NaN;
- });
-
- it('Defaults can be overridden for dates', async () => {
- const request = new Request('http://example.com/');
- const res = await app.render(request);
- const html = await res.text();
- const $ = cheerioLoad(html);
-
- const themeAdded = $($('.themes-list .theme-added')[1]).text();
- expect(new Date(themeAdded).getTime()).to.not.be.NaN;
- });
-
- it('Allows expression defaults for text columns', async () => {
- const request = new Request('http://example.com/');
- const res = await app.render(request);
- const html = await res.text();
- const $ = cheerioLoad(html);
+ it('Allows expression defaults for text columns', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerioLoad(html);
- const themeOwner = $($('.themes-list .theme-owner')[0]).text();
- expect(themeOwner).to.equal('');
- });
+ const themeOwner = $($('.themes-list .theme-owner')[0]).text();
+ expect(themeOwner).to.equal('');
+ });
- it('Allows expression defaults for boolean columns', async () => {
- const request = new Request('http://example.com/');
- const res = await app.render(request);
- const html = await res.text();
- const $ = cheerioLoad(html);
+ it('Allows expression defaults for boolean columns', async () => {
+ const html = await fixture.fetch('/').then((res) => res.text());
+ const $ = cheerioLoad(html);
- const themeDark = $($('.themes-list .theme-dark')[0]).text();
- expect(themeDark).to.equal('dark mode');
- });
+ const themeDark = $($('.themes-list .theme-dark')[0]).text();
+ expect(themeDark).to.equal('dark mode');
});
});
});
diff --git a/packages/db/test/fixtures/basics/astro.config.mjs b/packages/db/test/fixtures/basics/astro.config.mjs
new file mode 100644
index 000000000000..5ff1200e2418
--- /dev/null
+++ b/packages/db/test/fixtures/basics/astro.config.mjs
@@ -0,0 +1,7 @@
+import db from '@astrojs/db';
+import { defineConfig } from 'astro/config';
+
+// https://astro.build/config
+export default defineConfig({
+ integrations: [db()],
+});
diff --git a/packages/db/test/fixtures/basics/astro.config.ts b/packages/db/test/fixtures/basics/astro.config.ts
deleted file mode 100644
index 665568a82129..000000000000
--- a/packages/db/test/fixtures/basics/astro.config.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import db, { defineReadableTable, column } from '@astrojs/db';
-import { defineConfig } from 'astro/config';
-import { themes } from './themes-integration';
-
-const Author = defineReadableTable({
- columns: {
- name: column.text(),
- },
-});
-
-// https://astro.build/config
-export default defineConfig({
- integrations: [db(), themes()],
- db: {
- studio: false,
- unsafeWritable: true,
- tables: { Author },
- async data({ seed }) {
- await seed(Author, [
- { name: 'Ben' },
- { name: 'Nate' },
- { name: 'Erika' },
- { name: 'Bjorn' },
- { name: 'Sarah' },
- ]);
- },
- },
-});
diff --git a/packages/db/test/fixtures/basics/db/config.ts b/packages/db/test/fixtures/basics/db/config.ts
new file mode 100644
index 000000000000..f216caab6e45
--- /dev/null
+++ b/packages/db/test/fixtures/basics/db/config.ts
@@ -0,0 +1,12 @@
+import { Themes } from './theme';
+import { column, defineDB, defineTable } from 'astro:db';
+
+const Author = defineTable({
+ columns: {
+ name: column.text(),
+ },
+});
+
+export default defineDB({
+ tables: { Author, Themes },
+});
diff --git a/packages/db/test/fixtures/basics/db/seed.ts b/packages/db/test/fixtures/basics/db/seed.ts
new file mode 100644
index 000000000000..33d82d523782
--- /dev/null
+++ b/packages/db/test/fixtures/basics/db/seed.ts
@@ -0,0 +1,19 @@
+import { asDrizzleTable } from '@astrojs/db/utils';
+import { Themes as ThemesConfig } from './theme';
+import { Author, db } from 'astro:db';
+
+const Themes = asDrizzleTable('Themes', ThemesConfig);
+
+await db
+ .insert(Themes)
+ .values([{ name: 'dracula' }, { name: 'monokai', added: new Date() }])
+ .returning({ name: Themes.name });
+await db
+ .insert(Author)
+ .values([
+ { name: 'Ben' },
+ { name: 'Nate' },
+ { name: 'Erika' },
+ { name: 'Bjorn' },
+ { name: 'Sarah' },
+ ]);
diff --git a/packages/db/test/fixtures/basics/db/theme.ts b/packages/db/test/fixtures/basics/db/theme.ts
new file mode 100644
index 000000000000..aecc67f7d5ae
--- /dev/null
+++ b/packages/db/test/fixtures/basics/db/theme.ts
@@ -0,0 +1,15 @@
+import { NOW, column, defineTable, sql } from 'astro:db';
+
+export const Themes = defineTable({
+ columns: {
+ name: column.text(),
+ added: column.date({
+ default: sql`CURRENT_TIMESTAMP`,
+ }),
+ updated: column.date({
+ default: NOW,
+ }),
+ isDark: column.boolean({ default: sql`TRUE` }),
+ owner: column.text({ optional: true, default: sql`NULL` }),
+ },
+});
diff --git a/packages/db/test/fixtures/basics/package.json b/packages/db/test/fixtures/basics/package.json
index f537f998df79..af7cbe229800 100644
--- a/packages/db/test/fixtures/basics/package.json
+++ b/packages/db/test/fixtures/basics/package.json
@@ -2,6 +2,11 @@
"name": "@test/db-aliases",
"version": "0.0.0",
"private": true,
+ "scripts": {
+ "dev": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview"
+ },
"dependencies": {
"@astrojs/db": "workspace:*",
"astro": "workspace:*"
diff --git a/packages/db/test/fixtures/basics/src/pages/index.astro b/packages/db/test/fixtures/basics/src/pages/index.astro
index 4e8882d57e0a..2d21f81103b8 100644
--- a/packages/db/test/fixtures/basics/src/pages/index.astro
+++ b/packages/db/test/fixtures/basics/src/pages/index.astro
@@ -1,4 +1,5 @@
---
+///
import { Author, db, Themes } from 'astro:db';
const authors = await db.select().from(Author);
diff --git a/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro b/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro
deleted file mode 100644
index 7a6bb37bc3db..000000000000
--- a/packages/db/test/fixtures/basics/src/pages/insert-into-readonly.astro
+++ /dev/null
@@ -1,14 +0,0 @@
----
-import { Author, db } from 'astro:db';
-
-const authors = await db.select().from(Author);
-
-let error: any = {};
-try {
- db.insert(Author).values({ name: 'Person A' });
-} catch (err) {
- error = err;
-}
----
-
-{error.message}
diff --git a/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro b/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro
deleted file mode 100644
index 3e3ad2f31ac5..000000000000
--- a/packages/db/test/fixtures/basics/src/pages/insert-into-writable.astro
+++ /dev/null
@@ -1,12 +0,0 @@
----
-import { Themes, db } from 'astro:db';
-
-let error: any = {};
-try {
- db.insert(Themes).values({ name: 'Person A' });
-} catch (err) {
- error = err;
-}
----
-
-{error.message}
diff --git a/packages/db/test/fixtures/basics/themes-integration.ts b/packages/db/test/fixtures/basics/themes-integration.ts
deleted file mode 100644
index d034d840f85b..000000000000
--- a/packages/db/test/fixtures/basics/themes-integration.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { NOW, column, defineWritableTable, sql } from '@astrojs/db';
-import type { AstroIntegration } from 'astro';
-
-const Themes = defineWritableTable({
- columns: {
- name: column.text(),
- added: column.date({
- default: sql`CURRENT_TIMESTAMP`,
- }),
- updated: column.date({
- default: NOW,
- }),
- isDark: column.boolean({ default: sql`TRUE` }),
- owner: column.text({ optional: true, default: sql`NULL` }),
- },
-});
-
-export function themes(): AstroIntegration {
- return {
- name: 'themes-integration',
- hooks: {
- 'astro:config:setup': ({ updateConfig }) => {
- updateConfig({
- db: {
- tables: { Themes },
- async data({ seed }) {
- // Seed writable tables in dev mode, only
- // but in this case we do it for both, due to tests
- await seed(Themes, [{ name: 'dracula' }, { name: 'monokai', added: new Date() }]);
- },
- },
- });
- },
- },
- };
-}
diff --git a/packages/db/test/fixtures/glob/astro.config.ts b/packages/db/test/fixtures/glob/astro.config.ts
deleted file mode 100644
index d84117f4e6e3..000000000000
--- a/packages/db/test/fixtures/glob/astro.config.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import db, { defineReadableTable, column } from '@astrojs/db';
-import { defineConfig } from 'astro/config';
-import { asJson, createGlob } from './utils';
-
-const Quote = defineReadableTable({
- columns: {
- author: column.text(),
- body: column.text(),
- file: column.text({ unique: true }),
- },
-});
-
-export default defineConfig({
- db: {
- tables: { Quote },
- data({ seed, ...ctx }) {
- const glob = createGlob(ctx);
- glob('quotes/*.json', {
- into: Quote,
- parse: asJson,
- });
- },
- },
- integrations: [db()],
-});
diff --git a/packages/db/test/fixtures/glob/package.json b/packages/db/test/fixtures/glob/package.json
deleted file mode 100644
index d7db0815c927..000000000000
--- a/packages/db/test/fixtures/glob/package.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "name": "glob",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "scripts": {
- "dev": "astro dev",
- "build": "astro build",
- "preview": "astro preview"
- },
- "dependencies": {
- "@astrojs/db": "workspace:*",
- "astro": "workspace:*",
- "chokidar": "^3.5.3",
- "drizzle-orm": "^0.28.6",
- "fast-glob": "^3.3.2"
- },
- "keywords": [],
- "author": "",
- "license": "ISC"
-}
diff --git a/packages/db/test/fixtures/glob/quotes/erika.json b/packages/db/test/fixtures/glob/quotes/erika.json
deleted file mode 100644
index c7ece9ca6ee3..000000000000
--- a/packages/db/test/fixtures/glob/quotes/erika.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "author": "Erika",
- "body": "Put the quote in the database."
-}
diff --git a/packages/db/test/fixtures/glob/quotes/tony.json b/packages/db/test/fixtures/glob/quotes/tony.json
deleted file mode 100644
index 3aaed7746b83..000000000000
--- a/packages/db/test/fixtures/glob/quotes/tony.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "author": "Tony Sull",
- "body": "All content is data, but not all data is content."
-}
diff --git a/packages/db/test/fixtures/glob/src/pages/index.astro b/packages/db/test/fixtures/glob/src/pages/index.astro
deleted file mode 100644
index 6d2fdf22c0eb..000000000000
--- a/packages/db/test/fixtures/glob/src/pages/index.astro
+++ /dev/null
@@ -1,25 +0,0 @@
----
-///
-import { Quote, db } from 'astro:db';
-
-const quotes = await db.select().from(Quote);
----
-
-
-
-
-
-
- Document
-
-
- {
- quotes.map((q) => (
-
- ))
- }
-
-
diff --git a/packages/db/test/fixtures/glob/utils.ts b/packages/db/test/fixtures/glob/utils.ts
deleted file mode 100644
index 5602c610807c..000000000000
--- a/packages/db/test/fixtures/glob/utils.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import { type DBDataContext, type ResolvedCollectionConfig } from '@astrojs/db';
-import chokidar from 'chokidar';
-import { eq } from 'drizzle-orm';
-import fastGlob from 'fast-glob';
-import { readFile } from 'fs/promises';
-
-export function createGlob({ db, mode }: Pick) {
- return async function glob(
- pattern: string,
- opts: {
- into: ResolvedCollectionConfig;
- parse: (params: { file: string; content: string }) => Record;
- }
- ) {
- // TODO: expose `table`
- const { table } = opts.into as any;
- const fileColumn = table.file;
- if (!fileColumn) {
- throw new Error('`file` column is required for glob tables.');
- }
- if (mode === 'dev') {
- chokidar
- .watch(pattern)
- .on('add', async (file) => {
- const content = await readFile(file, 'utf-8');
- const parsed = opts.parse({ file, content });
- await db.insert(table).values({ ...parsed, file });
- })
- .on('change', async (file) => {
- const content = await readFile(file, 'utf-8');
- const parsed = opts.parse({ file, content });
- await db
- .insert(table)
- .values({ ...parsed, file })
- .onConflictDoUpdate({
- target: fileColumn,
- set: parsed,
- });
- })
- .on('unlink', async (file) => {
- await db.delete(table).where(eq(fileColumn, file));
- });
- } else {
- const files = await fastGlob(pattern);
- for (const file of files) {
- const content = await readFile(file, 'utf-8');
- const parsed = opts.parse({ file, content });
- await db.insert(table).values({ ...parsed, file });
- }
- }
- };
-}
-
-export function asJson(params: { file: string; content: string }) {
- try {
- return JSON.parse(params.content);
- } catch (e) {
- throw new Error(`Error parsing ${params.file}: ${e.message}`);
- }
-}
diff --git a/packages/db/test/fixtures/recipes/astro.config.ts b/packages/db/test/fixtures/recipes/astro.config.ts
index bb7ad225e4cd..bd6088769ffa 100644
--- a/packages/db/test/fixtures/recipes/astro.config.ts
+++ b/packages/db/test/fixtures/recipes/astro.config.ts
@@ -1,82 +1,6 @@
-import astroDb, { defineReadableTable, column } from '@astrojs/db';
+import astroDb from '@astrojs/db';
import { defineConfig } from 'astro/config';
-const Recipe = defineReadableTable({
- columns: {
- id: column.number({ primaryKey: true }),
- title: column.text(),
- description: column.text(),
- },
-});
-
-const Ingredient = defineReadableTable({
- columns: {
- id: column.number({ primaryKey: true }),
- name: column.text(),
- quantity: column.number(),
- recipeId: column.number(),
- },
- indexes: {
- recipeIdx: { on: 'recipeId' },
- },
- foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }],
-});
-
export default defineConfig({
integrations: [astroDb()],
- db: {
- tables: { Recipe, Ingredient },
- async data({ seed, seedReturning }) {
- const pancakes = await seedReturning(Recipe, {
- title: 'Pancakes',
- description: 'A delicious breakfast',
- });
-
- await seed(Ingredient, [
- {
- name: 'Flour',
- quantity: 1,
- recipeId: pancakes.id,
- },
- {
- name: 'Eggs',
- quantity: 2,
- recipeId: pancakes.id,
- },
- {
- name: 'Milk',
- quantity: 1,
- recipeId: pancakes.id,
- },
- ]);
-
- const pizza = await seedReturning(Recipe, {
- title: 'Pizza',
- description: 'A delicious dinner',
- });
-
- await seed(Ingredient, [
- {
- name: 'Flour',
- quantity: 1,
- recipeId: pizza.id,
- },
- {
- name: 'Eggs',
- quantity: 2,
- recipeId: pizza.id,
- },
- {
- name: 'Milk',
- quantity: 1,
- recipeId: pizza.id,
- },
- {
- name: 'Tomato Sauce',
- quantity: 1,
- recipeId: pizza.id,
- },
- ]);
- },
- },
});
diff --git a/packages/db/test/fixtures/recipes/db/config.ts b/packages/db/test/fixtures/recipes/db/config.ts
new file mode 100644
index 000000000000..6334ba8edcfa
--- /dev/null
+++ b/packages/db/test/fixtures/recipes/db/config.ts
@@ -0,0 +1,26 @@
+import { column, defineDB, defineTable } from 'astro:db';
+
+const Recipe = defineTable({
+ columns: {
+ id: column.number({ primaryKey: true }),
+ title: column.text(),
+ description: column.text(),
+ },
+});
+
+const Ingredient = defineTable({
+ columns: {
+ id: column.number({ primaryKey: true }),
+ name: column.text(),
+ quantity: column.number(),
+ recipeId: column.number(),
+ },
+ indexes: {
+ recipeIdx: { on: 'recipeId' },
+ },
+ foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }],
+});
+
+export default defineDB({
+ tables: { Recipe, Ingredient },
+});
diff --git a/packages/db/test/fixtures/recipes/db/seed.ts b/packages/db/test/fixtures/recipes/db/seed.ts
new file mode 100644
index 000000000000..7a4892376fde
--- /dev/null
+++ b/packages/db/test/fixtures/recipes/db/seed.ts
@@ -0,0 +1,60 @@
+import { Ingredient, Recipe, db } from 'astro:db';
+
+const pancakes = await db
+ .insert(Recipe)
+ .values({
+ title: 'Pancakes',
+ description: 'A delicious breakfast',
+ })
+ .returning()
+ .get();
+
+await db.insert(Ingredient).values([
+ {
+ name: 'Flour',
+ quantity: 1,
+ recipeId: pancakes.id,
+ },
+ {
+ name: 'Eggs',
+ quantity: 2,
+ recipeId: pancakes.id,
+ },
+ {
+ name: 'Milk',
+ quantity: 1,
+ recipeId: pancakes.id,
+ },
+]);
+
+const pizza = await db
+ .insert(Recipe)
+ .values({
+ title: 'Pizza',
+ description: 'A delicious dinner',
+ })
+ .returning()
+ .get();
+
+await db.insert(Ingredient).values([
+ {
+ name: 'Flour',
+ quantity: 1,
+ recipeId: pizza.id,
+ },
+ {
+ name: 'Eggs',
+ quantity: 2,
+ recipeId: pizza.id,
+ },
+ {
+ name: 'Milk',
+ quantity: 1,
+ recipeId: pizza.id,
+ },
+ {
+ name: 'Tomato Sauce',
+ quantity: 1,
+ recipeId: pizza.id,
+ },
+]);
diff --git a/packages/db/test/fixtures/ticketing-example/astro.config.ts b/packages/db/test/fixtures/ticketing-example/astro.config.ts
index 1f34f6f0d75a..616156f9af9c 100644
--- a/packages/db/test/fixtures/ticketing-example/astro.config.ts
+++ b/packages/db/test/fixtures/ticketing-example/astro.config.ts
@@ -1,32 +1,9 @@
-import db, { defineReadableTable, defineWritableTable, column } from '@astrojs/db';
+import db from '@astrojs/db';
import node from '@astrojs/node';
import react from '@astrojs/react';
import { defineConfig } from 'astro/config';
import simpleStackForm from 'simple-stack-form';
-const Event = defineReadableTable({
- columns: {
- id: column.number({
- primaryKey: true,
- }),
- name: column.text(),
- description: column.text(),
- ticketPrice: column.number(),
- date: column.date(),
- location: column.text(),
- },
-});
-const Ticket = defineWritableTable({
- columns: {
- eventId: column.number({ references: () => Event.columns.id }),
- email: column.text(),
- quantity: column.number(),
- newsletter: column.boolean({
- default: false,
- }),
- },
-});
-
// https://astro.build/config
export default defineConfig({
integrations: [simpleStackForm(), db(), react()],
@@ -34,23 +11,4 @@ export default defineConfig({
adapter: node({
mode: 'standalone',
}),
- db: {
- studio: true,
- tables: {
- Event,
- Ticket,
- },
- data({ seed }) {
- seed(Event, [
- {
- name: 'Sampha LIVE in Brooklyn',
- description:
- 'Sampha is on tour with his new, flawless album Lahai. Come see the live performance outdoors in Prospect Park. Yes, there will be a grand piano 🎹',
- date: new Date('2024-01-01'),
- ticketPrice: 10000,
- location: 'Brooklyn, NY',
- },
- ]);
- },
- },
});
diff --git a/packages/db/test/fixtures/ticketing-example/db/config.ts b/packages/db/test/fixtures/ticketing-example/db/config.ts
new file mode 100644
index 000000000000..f8148eaed305
--- /dev/null
+++ b/packages/db/test/fixtures/ticketing-example/db/config.ts
@@ -0,0 +1,27 @@
+import { column, defineDB, defineTable } from 'astro:db';
+
+const Event = defineTable({
+ columns: {
+ id: column.number({
+ primaryKey: true,
+ }),
+ name: column.text(),
+ description: column.text(),
+ ticketPrice: column.number(),
+ date: column.date(),
+ location: column.text(),
+ },
+});
+
+const Ticket = defineTable({
+ columns: {
+ eventId: column.number({ references: () => Event.columns.id }),
+ email: column.text(),
+ quantity: column.number(),
+ newsletter: column.boolean({
+ default: true,
+ }),
+ },
+});
+
+export default defineDB({ tables: { Event, Ticket } });
diff --git a/packages/db/test/fixtures/ticketing-example/db/seed.ts b/packages/db/test/fixtures/ticketing-example/db/seed.ts
new file mode 100644
index 000000000000..c9789cbd666d
--- /dev/null
+++ b/packages/db/test/fixtures/ticketing-example/db/seed.ts
@@ -0,0 +1,10 @@
+import { Event, db } from 'astro:db';
+
+await db.insert(Event).values({
+ name: 'Sampha LIVE in Brooklyn',
+ description:
+ 'Sampha is on tour with his new, flawless album Lahai. Come see the live performance outdoors in Prospect Park. Yes, there will be a grand piano 🎹',
+ date: new Date('2024-01-01'),
+ ticketPrice: 10000,
+ location: 'Brooklyn, NY',
+});
diff --git a/packages/db/test/unit/field-queries.test.js b/packages/db/test/unit/column-queries.test.js
similarity index 89%
rename from packages/db/test/unit/field-queries.test.js
rename to packages/db/test/unit/column-queries.test.js
index dc7945f8c8be..c4d60d5c6f37 100644
--- a/packages/db/test/unit/field-queries.test.js
+++ b/packages/db/test/unit/column-queries.test.js
@@ -4,16 +4,17 @@ import {
getCollectionChangeQueries,
getMigrationQueries,
} from '../../dist/core/cli/migration-queries.js';
-import { getCreateTableQuery } from '../../dist/core/queries.js';
-import { collectionSchema, column, defineReadableTable } from '../../dist/core/types.js';
+import { tableSchema } from '../../dist/core/types.js';
+import { column, defineTable } from '../../dist/runtime/config.js';
import { NOW } from '../../dist/runtime/index.js';
+import { getCreateTableQuery } from '../../dist/runtime/queries.js';
-const COLLECTION_NAME = 'Users';
+const TABLE_NAME = 'Users';
// `parse` to resolve schema transformations
// ex. convert column.date() to ISO strings
-const userInitial = collectionSchema.parse(
- defineReadableTable({
+const userInitial = tableSchema.parse(
+ defineTable({
columns: {
name: column.text(),
age: column.number(),
@@ -28,15 +29,11 @@ const defaultAmbiguityResponses = {
columnRenames: {},
};
-function userChangeQueries(
- oldCollection,
- newCollection,
- ambiguityResponses = defaultAmbiguityResponses
-) {
+function userChangeQueries(oldTable, newTable, ambiguityResponses = defaultAmbiguityResponses) {
return getCollectionChangeQueries({
- collectionName: COLLECTION_NAME,
- oldCollection,
- newCollection,
+ collectionName: TABLE_NAME,
+ oldCollection: oldTable,
+ newCollection: newTable,
ambiguityResponses,
});
}
@@ -56,35 +53,35 @@ function configChangeQueries(
describe('column queries', () => {
describe('getMigrationQueries', () => {
it('should be empty when tables are the same', async () => {
- const oldCollections = { [COLLECTION_NAME]: userInitial };
- const newCollections = { [COLLECTION_NAME]: userInitial };
+ const oldCollections = { [TABLE_NAME]: userInitial };
+ const newCollections = { [TABLE_NAME]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections);
expect(queries).to.deep.equal([]);
});
it('should create table for new tables', async () => {
const oldCollections = {};
- const newCollections = { [COLLECTION_NAME]: userInitial };
+ const newCollections = { [TABLE_NAME]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections);
- expect(queries).to.deep.equal([getCreateTableQuery(COLLECTION_NAME, userInitial)]);
+ expect(queries).to.deep.equal([getCreateTableQuery(TABLE_NAME, userInitial)]);
});
it('should drop table for removed tables', async () => {
- const oldCollections = { [COLLECTION_NAME]: userInitial };
+ const oldCollections = { [TABLE_NAME]: userInitial };
const newCollections = {};
const { queries } = await configChangeQueries(oldCollections, newCollections);
- expect(queries).to.deep.equal([`DROP TABLE "${COLLECTION_NAME}"`]);
+ expect(queries).to.deep.equal([`DROP TABLE "${TABLE_NAME}"`]);
});
it('should rename table for renamed tables', async () => {
const rename = 'Peeps';
- const oldCollections = { [COLLECTION_NAME]: userInitial };
+ const oldCollections = { [TABLE_NAME]: userInitial };
const newCollections = { [rename]: userInitial };
const { queries } = await configChangeQueries(oldCollections, newCollections, {
...defaultAmbiguityResponses,
- collectionRenames: { [rename]: COLLECTION_NAME },
+ collectionRenames: { [rename]: TABLE_NAME },
});
- expect(queries).to.deep.equal([`ALTER TABLE "${COLLECTION_NAME}" RENAME TO "${rename}"`]);
+ expect(queries).to.deep.equal([`ALTER TABLE "${TABLE_NAME}" RENAME TO "${rename}"`]);
});
});
@@ -95,14 +92,14 @@ describe('column queries', () => {
});
it('should be empty when type updated to same underlying SQL type', async () => {
- const blogInitial = collectionSchema.parse({
+ const blogInitial = tableSchema.parse({
...userInitial,
columns: {
title: column.text(),
draft: column.boolean(),
},
});
- const blogFinal = collectionSchema.parse({
+ const blogFinal = tableSchema.parse({
...userInitial,
columns: {
...blogInitial.columns,
@@ -114,7 +111,7 @@ describe('column queries', () => {
});
it('should respect user primary key without adding a hidden id', async () => {
- const user = collectionSchema.parse({
+ const user = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
@@ -122,7 +119,7 @@ describe('column queries', () => {
},
});
- const userFinal = collectionSchema.parse({
+ const userFinal = tableSchema.parse({
...user,
columns: {
...user.columns,
@@ -155,10 +152,10 @@ describe('column queries', () => {
const { queries } = await userChangeQueries(userInitial, userFinal, {
collectionRenames: {},
- columnRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } },
+ columnRenames: { [TABLE_NAME]: { middleInitial: 'mi' } },
});
expect(queries).to.deep.equal([
- `ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`,
+ `ALTER TABLE "${TABLE_NAME}" RENAME COLUMN "mi" TO "middleInitial"`,
]);
});
});
@@ -287,7 +284,7 @@ describe('column queries', () => {
});
it('when updating to a runtime default', async () => {
- const initial = collectionSchema.parse({
+ const initial = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
@@ -295,7 +292,7 @@ describe('column queries', () => {
},
});
- const userFinal = collectionSchema.parse({
+ const userFinal = tableSchema.parse({
...initial,
columns: {
...initial.columns,
@@ -317,7 +314,7 @@ describe('column queries', () => {
});
it('when adding a column with a runtime default', async () => {
- const userFinal = collectionSchema.parse({
+ const userFinal = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
@@ -407,7 +404,7 @@ describe('column queries', () => {
it('when adding a required column with default', async () => {
const defaultDate = new Date('2023-01-01');
- const userFinal = collectionSchema.parse({
+ const userFinal = tableSchema.parse({
...userInitial,
columns: {
...userInitial.columns,
diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.js
index 17b2599bdeb2..ad588959d173 100644
--- a/packages/db/test/unit/index-queries.test.js
+++ b/packages/db/test/unit/index-queries.test.js
@@ -1,9 +1,10 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js';
-import { collectionSchema, column } from '../../dist/core/types.js';
+import { tableSchema } from '../../dist/core/types.js';
+import { column } from '../../dist/runtime/config.js';
-const userInitial = collectionSchema.parse({
+const userInitial = tableSchema.parse({
columns: {
name: column.text(),
age: column.number(),
diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.js
index 4c2e3af8d6bb..a4b0bdd2d923 100644
--- a/packages/db/test/unit/reference-queries.test.js
+++ b/packages/db/test/unit/reference-queries.test.js
@@ -1,9 +1,10 @@
import { expect } from 'chai';
import { describe, it } from 'mocha';
import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js';
-import { column, defineReadableTable, tablesSchema } from '../../dist/core/types.js';
+import { tablesSchema } from '../../dist/core/types.js';
+import { column, defineTable } from '../../dist/runtime/config.js';
-const BaseUser = defineReadableTable({
+const BaseUser = defineTable({
columns: {
id: column.number({ primaryKey: true }),
name: column.text(),
@@ -13,7 +14,7 @@ const BaseUser = defineReadableTable({
},
});
-const BaseSentBox = defineReadableTable({
+const BaseSentBox = defineTable({
columns: {
to: column.number(),
toName: column.text(),
@@ -58,7 +59,7 @@ describe('reference queries', () => {
it('adds references with lossless table recreate', async () => {
const { SentBox: Initial } = resolveReferences();
const { SentBox: Final } = resolveReferences({
- SentBox: defineReadableTable({
+ SentBox: defineTable({
columns: {
...BaseSentBox.columns,
to: column.number({ references: () => BaseUser.columns.id }),
@@ -82,7 +83,7 @@ describe('reference queries', () => {
it('removes references with lossless table recreate', async () => {
const { SentBox: Initial } = resolveReferences({
- SentBox: defineReadableTable({
+ SentBox: defineTable({
columns: {
...BaseSentBox.columns,
to: column.number({ references: () => BaseUser.columns.id }),
@@ -108,7 +109,7 @@ describe('reference queries', () => {
it('does not use ADD COLUMN when adding optional column with reference', async () => {
const { SentBox: Initial } = resolveReferences();
const { SentBox: Final } = resolveReferences({
- SentBox: defineReadableTable({
+ SentBox: defineTable({
columns: {
...BaseSentBox.columns,
from: column.number({ references: () => BaseUser.columns.id, optional: true }),
@@ -131,13 +132,13 @@ describe('reference queries', () => {
it('adds and updates foreign key with lossless table recreate', async () => {
const { SentBox: InitialWithoutFK } = resolveReferences();
const { SentBox: InitialWithDifferentFK } = resolveReferences({
- SentBox: defineReadableTable({
+ SentBox: defineTable({
...BaseSentBox,
foreignKeys: [{ columns: ['to'], references: () => [BaseUser.columns.id] }],
}),
});
const { SentBox: Final } = resolveReferences({
- SentBox: defineReadableTable({
+ SentBox: defineTable({
...BaseSentBox,
foreignKeys: [
{
diff --git a/packages/integrations/markdoc/CHANGELOG.md b/packages/integrations/markdoc/CHANGELOG.md
index 3acfb43cece9..fcfb941e8976 100644
--- a/packages/integrations/markdoc/CHANGELOG.md
+++ b/packages/integrations/markdoc/CHANGELOG.md
@@ -1,5 +1,11 @@
# @astrojs/markdoc
+## 0.9.1
+
+### Patch Changes
+
+- [#10278](https://github.com/withastro/astro/pull/10278) [`a548a3a99c2835c19662fc38636f92b2bda26614`](https://github.com/withastro/astro/commit/a548a3a99c2835c19662fc38636f92b2bda26614) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes original images sometimes being kept / deleted when they shouldn't in both MDX and Markdoc
+
## 0.9.0
### Minor Changes
diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json
index 9d82609720e4..0d0917b58631 100644
--- a/packages/integrations/markdoc/package.json
+++ b/packages/integrations/markdoc/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/markdoc",
"description": "Add support for Markdoc in your Astro site",
- "version": "0.9.0",
+ "version": "0.9.1",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
diff --git a/packages/integrations/markdoc/src/content-entry-type.ts b/packages/integrations/markdoc/src/content-entry-type.ts
index fe15d03b01ea..89f9f9e8687b 100644
--- a/packages/integrations/markdoc/src/content-entry-type.ts
+++ b/packages/integrations/markdoc/src/content-entry-type.ts
@@ -196,7 +196,19 @@ async function emitOptimizedImages(
ctx.pluginContext.meta.watchMode,
ctx.pluginContext.emitFile
);
- node.attributes[attributeName] = src;
+
+ const fsPath = resolved.id;
+
+ if (src) {
+ // We cannot track images in Markdoc, Markdoc rendering always strips out the proxy. As such, we'll always
+ // assume that the image is referenced elsewhere, to be on safer side.
+ if (ctx.astroConfig.output === 'static') {
+ if (globalThis.astroAsset.referencedImages)
+ globalThis.astroAsset.referencedImages.add(fsPath);
+ }
+
+ node.attributes[attributeName] = { ...src, fsPath };
+ }
} else {
throw new MarkdocError({
message: `Could not resolve image ${JSON.stringify(
diff --git a/packages/integrations/node/CHANGELOG.md b/packages/integrations/node/CHANGELOG.md
index d95cb355c814..5d4f27635f64 100644
--- a/packages/integrations/node/CHANGELOG.md
+++ b/packages/integrations/node/CHANGELOG.md
@@ -1,5 +1,17 @@
# @astrojs/node
+## 8.2.3
+
+### Patch Changes
+
+- [#10285](https://github.com/withastro/astro/pull/10285) [`d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f`](https://github.com/withastro/astro/commit/d5277df5a4d1e9a8a7b6c8d7b87912e13a163f7f) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Fixes an issue where malformed requests could cause the server to error in certain cases.
+
+## 8.2.2
+
+### Patch Changes
+
+- [#10282](https://github.com/withastro/astro/pull/10282) [`b47dcaa25968ec85ba96fce23381c94a94e389f6`](https://github.com/withastro/astro/commit/b47dcaa25968ec85ba96fce23381c94a94e389f6) Thanks [@SatanshuMishra](https://github.com/SatanshuMishra)! - Fixes the `server.host` option to properly listen on all network interfaces when set to `true`
+
## 8.2.1
### Patch Changes
diff --git a/packages/integrations/node/package.json b/packages/integrations/node/package.json
index fea5c39b1cb4..0f9d55cc7207 100644
--- a/packages/integrations/node/package.json
+++ b/packages/integrations/node/package.json
@@ -1,7 +1,7 @@
{
"name": "@astrojs/node",
"description": "Deploy your site to a Node.js server",
- "version": "8.2.1",
+ "version": "8.2.3",
"type": "module",
"types": "./dist/index.d.ts",
"author": "withastro",
diff --git a/packages/integrations/node/src/serve-app.ts b/packages/integrations/node/src/serve-app.ts
index f2fc61f010d4..a9840b72148c 100644
--- a/packages/integrations/node/src/serve-app.ts
+++ b/packages/integrations/node/src/serve-app.ts
@@ -8,7 +8,15 @@ import type { RequestHandler } from './types.js';
*/
export function createAppHandler(app: NodeApp): RequestHandler {
return async (req, res, next, locals) => {
- const request = NodeApp.createRequest(req);
+ let request;
+ try {
+ request = NodeApp.createRequest(req);
+ } catch (err) {
+ res.statusCode = 500;
+ res.end('Internal Server Error');
+ return;
+ }
+
const routeData = app.match(request);
if (routeData) {
const response = await app.render(request, {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f0eb28d9453b..e9caf5b9b10c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -134,7 +134,7 @@ importers:
examples/basics:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/blog:
@@ -149,13 +149,13 @@ importers:
specifier: ^3.1.1
version: link:../../packages/integrations/sitemap
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/component:
devDependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/framework-alpine:
@@ -170,7 +170,7 @@ importers:
specifier: ^3.13.3
version: 3.13.3
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/framework-lit:
@@ -182,7 +182,7 @@ importers:
specifier: ^0.2.1
version: 0.2.1
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
lit:
specifier: ^3.1.2
@@ -206,7 +206,7 @@ importers:
specifier: ^4.0.8
version: link:../../packages/integrations/vue
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
preact:
specifier: ^10.19.2
@@ -236,7 +236,7 @@ importers:
specifier: ^1.2.1
version: 1.2.1(preact@10.19.3)
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
preact:
specifier: ^10.19.2
@@ -254,7 +254,7 @@ importers:
specifier: ^18.2.15
version: 18.2.18
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
react:
specifier: ^18.2.0
@@ -269,7 +269,7 @@ importers:
specifier: ^4.0.1
version: link:../../packages/integrations/solid
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
solid-js:
specifier: ^1.8.5
@@ -281,7 +281,7 @@ importers:
specifier: ^5.2.0
version: link:../../packages/integrations/svelte
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
svelte:
specifier: ^4.2.5
@@ -293,7 +293,7 @@ importers:
specifier: ^4.0.8
version: link:../../packages/integrations/vue
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
vue:
specifier: ^3.3.8
@@ -302,25 +302,25 @@ importers:
examples/hackernews:
dependencies:
'@astrojs/node':
- specifier: ^8.2.1
+ specifier: ^8.2.3
version: link:../../packages/integrations/node
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/integration:
devDependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/middleware:
dependencies:
'@astrojs/node':
- specifier: ^8.2.1
+ specifier: ^8.2.3
version: link:../../packages/integrations/node
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
html-minifier:
specifier: ^4.0.0
@@ -333,31 +333,31 @@ importers:
examples/minimal:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/non-html-pages:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/portfolio:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/ssr:
dependencies:
'@astrojs/node':
- specifier: ^8.2.1
+ specifier: ^8.2.3
version: link:../../packages/integrations/node
'@astrojs/svelte':
specifier: ^5.2.0
version: link:../../packages/integrations/svelte
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
svelte:
specifier: ^4.2.5
@@ -366,7 +366,7 @@ importers:
examples/starlog:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
sass:
specifier: ^1.69.5
@@ -378,22 +378,22 @@ importers:
examples/view-transitions:
devDependencies:
'@astrojs/node':
- specifier: ^8.2.1
+ specifier: ^8.2.3
version: link:../../packages/integrations/node
'@astrojs/tailwind':
specifier: ^5.1.0
version: link:../../packages/integrations/tailwind
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/with-markdoc:
dependencies:
'@astrojs/markdoc':
- specifier: ^0.9.0
+ specifier: ^0.9.1
version: link:../../packages/integrations/markdoc
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/with-markdown-plugins:
@@ -402,7 +402,7 @@ importers:
specifier: ^4.2.1
version: link:../../packages/markdown/remark
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
hast-util-select:
specifier: ^6.0.2
@@ -423,7 +423,7 @@ importers:
examples/with-markdown-shiki:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
examples/with-mdx:
@@ -435,7 +435,7 @@ importers:
specifier: ^3.1.1
version: link:../../packages/integrations/preact
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
preact:
specifier: ^10.19.2
@@ -450,7 +450,7 @@ importers:
specifier: ^0.5.0
version: 0.5.0(nanostores@0.9.5)(preact@10.19.3)
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
nanostores:
specifier: ^0.9.5
@@ -471,7 +471,7 @@ importers:
specifier: ^1.6.3
version: 1.6.4
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
autoprefixer:
specifier: ^10.4.15
@@ -489,7 +489,7 @@ importers:
examples/with-vitest:
dependencies:
astro:
- specifier: ^4.4.6
+ specifier: ^4.4.9
version: link:../../packages/astro
vitest:
specifier: ^1.3.1
@@ -2516,6 +2516,12 @@ importers:
packages/astro/test/fixtures/core-image-deletion:
dependencies:
+ '@astrojs/markdoc':
+ specifier: workspace:*
+ version: link:../../../../integrations/markdoc
+ '@astrojs/mdx':
+ specifier: workspace:*
+ version: link:../../../../integrations/mdx
astro:
specifier: workspace:*
version: link:../../..
@@ -3885,24 +3891,6 @@ importers:
specifier: workspace:*
version: link:../../../../astro
- packages/db/test/fixtures/glob:
- dependencies:
- '@astrojs/db':
- specifier: workspace:*
- version: link:../../..
- astro:
- specifier: workspace:*
- version: link:../../../../astro
- chokidar:
- specifier: ^3.5.3
- version: 3.5.3
- drizzle-orm:
- specifier: ^0.28.6
- version: 0.28.6(@libsql/client@0.4.3)
- fast-glob:
- specifier: ^3.3.2
- version: 3.3.2
-
packages/db/test/fixtures/recipes:
dependencies:
'@astrojs/db':