mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-27 23:27:09 +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.
153 lines
4.2 KiB
TypeScript
153 lines
4.2 KiB
TypeScript
import type { Change } from "immerhin";
|
|
import { produceWithPatches, type Patch } from "immer";
|
|
import type {
|
|
BuilderPatch,
|
|
BuilderPatchChange,
|
|
} from "@webstudio-is/project-build/contracts";
|
|
import type { WebstudioData } from "@webstudio-is/sdk";
|
|
import type {
|
|
MarketplaceProduct,
|
|
ProjectSettings,
|
|
} from "@webstudio-is/project-build";
|
|
import * as builderStatePatch from "@webstudio-is/project-build/state";
|
|
import { serverSyncStore } from "./sync-stores";
|
|
|
|
type BuilderPatchData = WebstudioData & {
|
|
marketplaceProduct?: MarketplaceProduct;
|
|
projectSettings?: ProjectSettings;
|
|
};
|
|
|
|
const getNamespaceData = (
|
|
data: BuilderPatchData,
|
|
namespace: BuilderPatchChange["namespace"]
|
|
) => {
|
|
const namespaceData = data[namespace];
|
|
if (namespaceData !== undefined) {
|
|
return namespaceData;
|
|
}
|
|
throw Error(`Cannot apply patch for unavailable namespace "${namespace}"`);
|
|
};
|
|
|
|
const applyBuilderPatchPayloadMutable = (
|
|
data: BuilderPatchData,
|
|
payload: readonly BuilderPatchChange[]
|
|
) => {
|
|
const applicableChanges: BuilderPatchChange[] = [];
|
|
for (const change of payload) {
|
|
const [patch] = change.patches;
|
|
if (
|
|
change.namespace === "marketplaceProduct" &&
|
|
data.marketplaceProduct === undefined &&
|
|
change.patches.length === 1 &&
|
|
patch?.op === "replace" &&
|
|
patch.path.length === 0
|
|
) {
|
|
data.marketplaceProduct = structuredClone(
|
|
patch.value
|
|
) as MarketplaceProduct;
|
|
continue;
|
|
}
|
|
applicableChanges.push(change);
|
|
}
|
|
builderStatePatch.applyBuilderPatchPayloadMutable(
|
|
(namespace) => getNamespaceData(data, namespace),
|
|
applicableChanges
|
|
);
|
|
};
|
|
|
|
const clonePatch = (patch: Patch | BuilderPatch): Patch =>
|
|
patch.op === "remove"
|
|
? { op: patch.op, path: [...patch.path] }
|
|
: {
|
|
op: patch.op,
|
|
path: [...patch.path],
|
|
value: structuredClone(patch.value),
|
|
};
|
|
|
|
const cloneBuilderPatch = (patch: BuilderPatch): BuilderPatch =>
|
|
patch.op === "remove"
|
|
? { op: patch.op, path: [...patch.path] }
|
|
: {
|
|
op: patch.op,
|
|
path: [...patch.path],
|
|
value: structuredClone(patch.value),
|
|
};
|
|
|
|
const createSyncChangesFromBuilderPatches = ({
|
|
payload,
|
|
revisePatches,
|
|
}: {
|
|
payload: readonly BuilderPatchChange[];
|
|
revisePatches: readonly Patch[];
|
|
}): Change[] => {
|
|
const changes = new Map<
|
|
BuilderPatchChange["namespace"],
|
|
{ patches: Patch[]; revisePatches: Patch[] }
|
|
>();
|
|
const getChange = (namespace: BuilderPatchChange["namespace"]) => {
|
|
const change = changes.get(namespace) ?? {
|
|
patches: [],
|
|
revisePatches: [],
|
|
};
|
|
changes.set(namespace, change);
|
|
return change;
|
|
};
|
|
for (const { namespace, patches } of payload) {
|
|
if (patches.length === 0) {
|
|
continue;
|
|
}
|
|
const change = getChange(namespace);
|
|
change.patches.push(...patches.map(cloneBuilderPatch));
|
|
}
|
|
for (const patch of revisePatches) {
|
|
const [namespace, ...path] = patch.path;
|
|
if (typeof namespace !== "string") {
|
|
continue;
|
|
}
|
|
const change = getChange(namespace as BuilderPatchChange["namespace"]);
|
|
if (
|
|
namespace === "marketplaceProduct" &&
|
|
patch.op === "remove" &&
|
|
path.length === 0
|
|
) {
|
|
change.revisePatches.push({ op: "replace", path, value: undefined });
|
|
continue;
|
|
}
|
|
change.revisePatches.push(clonePatch({ ...patch, path }));
|
|
}
|
|
return Array.from(changes, ([namespace, change]) => ({
|
|
namespace,
|
|
patches: change.patches,
|
|
revisePatches: change.revisePatches,
|
|
}));
|
|
};
|
|
|
|
export const createSyncChangesFromBuilderPatchPayload = ({
|
|
data,
|
|
payload,
|
|
}: {
|
|
data: BuilderPatchData;
|
|
payload: BuilderPatchChange[];
|
|
}): Change[] => {
|
|
const payloadForReversePatches = payload.map((change) => ({
|
|
namespace: change.namespace,
|
|
patches: change.patches.map(cloneBuilderPatch),
|
|
}));
|
|
const [, , revisePatches] = produceWithPatches(data, (draft) => {
|
|
applyBuilderPatchPayloadMutable(draft, payloadForReversePatches);
|
|
});
|
|
return createSyncChangesFromBuilderPatches({ payload, revisePatches });
|
|
};
|
|
|
|
export const createTransactionFromBuilderPatchPayload = ({
|
|
data,
|
|
payload,
|
|
}: {
|
|
data: BuilderPatchData;
|
|
payload: BuilderPatchChange[];
|
|
}) => {
|
|
serverSyncStore.createTransactionFromChanges(
|
|
createSyncChangesFromBuilderPatchPayload({ data, payload })
|
|
);
|
|
};
|