Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, expect, test } from "vitest";
import { computeExpression } from "@webstudio-is/project-build/runtime";
import {
createAssetQueryResourceBody,
getAssetIndexStatusLabel,
getAssetFileTypeGroqPredicate,
getAssetQueryConfigurationError,
isEmptyAssetQueryResult,
parseAssetQueryResourceBody,
} from "./asset-query-form-utils";

describe("asset query resource body", () => {
test("preserves runtime parameter bindings as expressions", () => {
const body = createAssetQueryResourceBody({
query: "*[properties.slug == $slug]",
parameters: [
{ name: "slug", value: "$ws$dataSource$routeSlug" },
{ name: "locale", value: '"en"' },
],
resultLimit: 1,
});

expect(
computeExpression(body, new Map([["routeSlug", "hello-world"]]))
).toEqual({
query: "*[properties.slug == $slug]",
parameters: { slug: "hello-world", locale: "en" },
resultLimit: 1,
content: { mode: "none" },
});
expect(parseAssetQueryResourceBody(body)).toEqual({
queryExpression: '"*[properties.slug == $slug]"',
resultLimitExpression: "1",
contentExpression: '{ "mode": "none" }',
parameters: [
{ name: "slug", value: "$ws$dataSource$routeSlug" },
{ name: "locale", value: '"en"' },
],
});
});

test("serializes result and selected-file hydration limits", () => {
const body = createAssetQueryResourceBody({
query: "*[0]{_id, revision, contentRef}",
parameters: [],
resultLimit: 1,
contentExpression: JSON.stringify({
mode: "range",
offset: 100,
length: 500,
}),
});

expect(computeExpression(body, new Map())).toEqual({
query: "*[0]{_id, revision, contentRef}",
parameters: {},
resultLimit: 1,
content: { mode: "range", offset: 100, length: 500 },
});
expect(parseAssetQueryResourceBody(body)).toMatchObject({
resultLimitExpression: "1",
contentExpression: '{"mode":"range","offset":100,"length":500}',
});
});

test("classifies index and empty-result preview states", () => {
const base = {
resourceId: "posts",
queryHash: "query-revision",
assetRevision: "asset-revision",
updatedAt: "2026-07-18T12:00:00.000Z",
};
expect(getAssetIndexStatusLabel(undefined)).toContain("No index");
expect(getAssetIndexStatusLabel({ ...base, state: "indexing" })).toContain(
"first index"
);
expect(getAssetIndexStatusLabel({ ...base, state: "stale" })).toContain(
"stale"
);
expect(getAssetIndexStatusLabel({ ...base, state: "failed" })).toContain(
"failed"
);
expect(
getAssetIndexStatusLabel({
...base,
state: "active",
activeRevision: "active-revision",
})
).toContain("active");
expect(isEmptyAssetQueryResult([])).toBe(true);
expect(isEmptyAssetQueryResult(null)).toBe(true);
expect(isEmptyAssetQueryResult({})).toBe(false);
});

test("file-type helpers produce visible GROQ source", () => {
expect(getAssetFileTypeGroqPredicate("md")).toBe('extension == "md"');
});

test("rejects invalid and duplicate query options before saving", () => {
expect(
getAssetQueryConfigurationError({
query: "*[]",
parameters: [{ name: "slug" }, { name: "slug" }],
resultLimit: 1,
content: { mode: "none" },
})
).toContain("unique");
expect(
getAssetQueryConfigurationError({
query: "*[]",
parameters: [],
resultLimit: 0,
content: { mode: "none" },
})
).toContain("Result limit");
expect(
getAssetQueryConfigurationError({
query: "*[properties.slug == $slug]",
parameters: [{ name: "slug" }],
resultLimit: 1,
content: { mode: "range", offset: 0, length: 10 },
})
).toBeUndefined();
expect(
getAssetQueryConfigurationError({
query: "*[properties.slug == $slug]",
parameters: [{ name: " slug " }],
resultLimit: 1,
content: { mode: "none" },
})
).toBeUndefined();
expect(
getAssetQueryConfigurationError({
query: "*[properties.slug == $slug]",
parameters: [],
resultLimit: 1,
content: { mode: "none" },
})
).toContain("$slug");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
assetResourceParameterName,
assetResourceContentOptions,
assetResourceLimits,
normalizeAssetQueryParameterName,
type AssetResourceContentOptions,
type AssetResourceIndexStatus,
} from "@webstudio-is/sdk";
import { validateAssetResourceQuery } from "@webstudio-is/asset-resource";
export {
createAssetQueryResourceBody,
parseAssetQueryResourceBody,
} from "@webstudio-is/sdk";
export type { AssetQueryParameterBinding } from "@webstudio-is/sdk";

export const getAssetIndexStatusLabel = (
status: AssetResourceIndexStatus | undefined
) => {
if (status === undefined) {
return "No index has been created yet.";
}
if (status.state === "indexing") {
return status.activeRevision === undefined
? "Building the first index…"
: "Building a replacement index; the last active revision is preserved.";
}
if (status.state === "stale") {
return "The saved query index is stale.";
}
if (status.state === "failed") {
return status.activeRevision === undefined
? "The index build failed."
: "The replacement build failed; the last active revision is preserved.";
}
return "The query index is active.";
};

export const isEmptyAssetQueryResult = (result: unknown) =>
result === null || (Array.isArray(result) && result.length === 0);

export const getAssetFileTypeGroqPredicate = (extension: string) =>
`extension == ${JSON.stringify(extension)}`;

export const getAssetQueryConfigurationError = ({
query,
parameters,
resultLimit,
content,
}: {
query: string;
parameters: readonly { name: string }[];
resultLimit: number;
content: AssetResourceContentOptions;
}) => {
let referencedParameterNames: string[];
try {
referencedParameterNames = validateAssetResourceQuery(query).parameterNames;
} catch {
return "Enter a valid GROQ query.";
}
if (parameters.length > assetResourceLimits.parameterCount) {
return `Use at most ${assetResourceLimits.parameterCount} runtime parameters.`;
}
const names = parameters.map(({ name }) =>
normalizeAssetQueryParameterName(name)
);
if (
names.some(
(name) => assetResourceParameterName.safeParse(name).success === false
)
) {
return "Runtime parameter names must be valid identifiers.";
}
if (new Set(names).size !== names.length) {
return "Runtime parameter names must be unique.";
}
const configuredNames = new Set(names);
const missingName = referencedParameterNames.find(
(name) => configuredNames.has(name) === false
);
if (missingName !== undefined) {
return `Add a runtime binding for $${missingName}.`;
}
if (
Number.isSafeInteger(resultLimit) === false ||
resultLimit < 1 ||
resultLimit > assetResourceLimits.resultCount
) {
return `Result limit must be between 1 and ${assetResourceLimits.resultCount}.`;
}
if (assetResourceContentOptions.safeParse(content).success === false) {
return "Selected-file content options are outside the supported limits.";
}
};
Loading
Loading