Files
webstudio/apps/builder/app/services/content-database.server.test.ts
Oleg Isonen 3ccb46cec7 fix: restore Marketplace settings and optimize Assets publishing (#5964)
## Outcome

Restore the Marketplace section for projects without existing
marketplace metadata, make the complete review-submission workflow
available over MCP, clarify shared Assets database diagnostics, and
reduce published database size by materializing fully static Assets
queries.

## Implementation

### Marketplace and MCP

- Initialize the Marketplace form from a safe partial default instead of
rendering nothing when metadata is unconfigured.
- Keep review submission disabled until the complete metadata schema is
valid.
- Allow `get-marketplace-product` to return `null` and
`update-marketplace-product` to create the first marketplace product
through ProjectSession/MCP.
- Add a confirmed `submit-marketplace-product` server operation that
validates configured metadata before setting review status to `PENDING`.
- Document the MCP workflow: upload the thumbnail, update metadata,
publish production, then submit.
- Correct the Marketplace thumbnail upload example to use the current
MCP input field.

### Assets database

- Show query size and merged published-database size separately in
resource diagnostics.
- Clarify that only the merged database counts toward the publish limit.
- Allow the Assets query editor to scroll long query source.
- Materialize fully static metadata-only Assets queries at publish time.
- Store materialized results as compact field columns and rows keyed by
a deterministic normalized-query hash.
- Serve materialized rows through the existing Assets response contract
without candidate documents or a runtime field catalog.
- Keep route- or variable-dependent queries on the existing
document-backed runtime path.
- Exclude overview-only fields from dynamic detail documents so the
merged database does not duplicate them.
- Preserve byte-limit selection, truncation diagnostics, integrity
verification, caching, and backward compatibility with existing
artifacts.

### Release

- Keep the release lockfile stable when preparing the CLI package.
- Regenerate runtime contracts, server-only API metadata, and CLI
documentation where required.

## Todo

- [x] Restore the empty Marketplace settings state
- [x] Prevent empty review submissions
- [x] Support first-time Marketplace metadata creation over MCP
- [x] Add MCP review submission
- [x] Document the end-to-end MCP workflow
- [x] Correct query and merged-database diagnostics
- [x] Make long Assets query source scrollable
- [x] Materialize static overview queries
- [x] Preserve dynamic detail-query behavior
- [x] Add regression coverage with fail/pass proof
- [x] Run relevant package and integration checks

## Verification

- [x] Content engine tests: 158 passed
- [x] Asset uploader tests: 179 passed
- [x] SDK tests: 505 passed
- [x] CLI tests: 921 passed
- [x] CLI prebuild integration tests: 46 passed
- [x] Builder Assets resource and diagnostics tests: 14 passed
- [x] Project runtime and ProjectSession tests: 133 passed
- [x] Builder API router tests: 30 passed
- [x] Protocol operation tests: 18 passed
- [x] Content engine build and declaration generation
- [x] Content engine, asset uploader, SDK, Builder, and CLI typechecks
- [x] Repository lint and formatting checks
- [x] Generated API and documentation checks
- [x] New materialization tests demonstrated failing with the
implementation disabled and passing after restoration

## Notes

- The representative five-post overview fixture shrinks from 2,122 bytes
to 1,197 bytes while returning the same sorted and paginated result.
- Materialization is intentionally limited to static, metadata-only
field queries without runtime asset fields. Unsupported or
data-incompatible queries safely use the existing runtime database.
- The complete `pnpm evaluations` agent suite was not run because
repository policy requires separate explicit approval.
- No database schema or migration changes are required.
2026-07-31 13:16:21 +02:00

200 lines
5.8 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { createPublishedProjectBundleFixture } from "@webstudio-is/protocol/fixtures";
import { createStructuredAssetQueryResourceBody } from "@webstudio-is/sdk";
import {
compileContentArtifact,
createAssetIndex,
createCanonicalAssetFileEntry,
} from "@webstudio-is/content-engine/compiler";
import { getContentDatabasePublishDiagnostics } from "./content-database.server";
describe("content database publish diagnostics", () => {
test("classifies affected dynamic and static resources individually", async () => {
const resourceId = "blog-posts";
const overviewResourceId = "blog-overview";
const createEntry = (id: string, bytes: number) =>
createCanonicalAssetFileEntry({
projectId: "source-project",
document: {
_id: id,
_type: "asset.file",
name: `${id}.md`,
path: `blog/${id}.md`,
key: id,
extension: "md",
mimeType: "text/markdown",
size: bytes,
revision: `revision-${id}`,
contentRef: `files/${id}.md`,
properties: { value: "x".repeat(bytes) },
},
});
const assetIndex = await createAssetIndex({
projectId: "source-project",
entries: [createEntry("small", 100), createEntry("large", 10_000)],
maxBytes: 5_000,
});
const bundle = createPublishedProjectBundleFixture({
build: {
props: [
[
"posts-prop",
{
id: "posts-prop",
instanceId: "collection",
name: "data",
type: "resource",
value: resourceId,
},
],
[
"overview-prop",
{
id: "overview-prop",
instanceId: "collection",
name: "overview",
type: "resource",
value: overviewResourceId,
},
],
],
resources: [
[
resourceId,
{
id: resourceId,
name: "Blog posts",
control: "system",
method: "post",
url: '"/$resources/assets"',
headers: [],
body: createStructuredAssetQueryResourceBody({
where: { all: [] },
sort: [],
limit: "pageSize",
offset: "0",
output: { mode: "base", includeMetadata: true },
content: { mode: "none" },
}),
},
],
[
overviewResourceId,
{
id: overviewResourceId,
name: "Blog overview",
control: "system",
method: "post",
url: '"/$resources/assets"',
headers: [],
body: createStructuredAssetQueryResourceBody({
where: { all: [] },
sort: [],
limit: "10",
offset: "0",
output: { mode: "base", includeMetadata: true },
content: { mode: "none" },
}),
},
],
],
},
assetIndex,
});
expect(getContentDatabasePublishDiagnostics(bundle)).toMatchObject({
stats: {
includedDocumentCount: 1,
omittedDocumentCount: 1,
maxBytes: 5_000,
omissionReason: "size",
truncated: true,
},
affectedResources: [
{ name: "Blog posts", kind: "dynamic" },
{ name: "Blog overview", kind: "static" },
],
});
});
test("does nothing when the build has no content query", () => {
expect(
getContentDatabasePublishDiagnostics(
createPublishedProjectBundleFixture()
)
).toBeUndefined();
});
test("reports unavailable content for affected static resources", async () => {
const resourceId = "blog-overview";
const { artifact: assetIndex } = await compileContentArtifact({
projectId: "source-project",
entries: [
{
...createCanonicalAssetFileEntry({
projectId: "source-project",
document: {
_id: "post",
_type: "asset.file",
name: "post.md",
path: "blog/post.md",
key: "post",
extension: "md",
mimeType: "text/markdown",
size: 100,
revision: "revision-post",
contentRef: "files/post.md",
properties: {},
},
}),
contentRequired: true,
},
],
maxBytes: 5_000,
});
const bundle = createPublishedProjectBundleFixture({
build: {
props: [
[
"overview-prop",
{
id: "overview-prop",
instanceId: "collection",
name: "data",
type: "resource",
value: resourceId,
},
],
],
resources: [
[
resourceId,
{
id: resourceId,
name: "Blog overview",
control: "system",
method: "post",
url: '"/$resources/assets"',
headers: [],
body: createStructuredAssetQueryResourceBody({
where: { all: [] },
sort: [],
limit: "10",
offset: "0",
output: { mode: "base", includeMetadata: true },
content: { mode: "none" },
}),
},
],
],
},
assetIndex,
});
expect(getContentDatabasePublishDiagnostics(bundle)).toMatchObject({
stats: { omissionReason: "unavailable", truncated: true },
affectedResources: [{ name: "Blog overview", kind: "static" }],
});
});
});