mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
## 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.
200 lines
5.8 KiB
TypeScript
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" }],
|
|
});
|
|
});
|
|
});
|