fix(knowledge-fs): handle initial revisions and published evidence

This commit is contained in:
Jyong 2026-08-02 14:47:09 -04:00
parent afee5e28d2
commit 1648d8ac6b
21 changed files with 305 additions and 26 deletions

View File

@ -99,6 +99,7 @@ from services.knowledge_fs.product_dto import (
KnowledgeFSGoldenQuestionResponse,
KnowledgeFSIdempotencyHeader,
KnowledgeFSJWKSResponse,
KnowledgeFSLogicalDocumentDeletePayload,
KnowledgeFSLogicalDocumentListResponse,
KnowledgeFSLogicalDocumentResponse,
KnowledgeFSMembersReplacePayload,
@ -207,6 +208,7 @@ register_schema_models(
KnowledgeFSBulkDocumentDeletePayload,
KnowledgeFSDocumentChunkListQuery,
KnowledgeFSDocumentDeletePayload,
KnowledgeFSLogicalDocumentDeletePayload,
KnowledgeFSDocumentMetadataPayload,
KnowledgeFSDocumentReindexPayload,
KnowledgeFSGoldenQuestionBulkImportPayload,
@ -1126,7 +1128,7 @@ class KnowledgeFSSpaceLogicalDocumentApi(Resource):
)
return dump_response(KnowledgeFSLogicalDocumentResponse, result)
@console_ns.expect(console_ns.models[KnowledgeFSDocumentDeletePayload.__name__])
@console_ns.expect(console_ns.models[KnowledgeFSLogicalDocumentDeletePayload.__name__])
@console_ns.doc(params=_IDEMPOTENCY_HEADER_PARAMS)
@console_ns.response(
HTTPStatus.ACCEPTED,
@ -1144,7 +1146,7 @@ class KnowledgeFSSpaceLogicalDocumentApi(Resource):
account_id=actor_id,
control_space_id=control_space_id,
document_id=document_id,
payload=_payload(KnowledgeFSDocumentDeletePayload),
payload=_payload(KnowledgeFSLogicalDocumentDeletePayload),
idempotency_key=_idempotency_key(),
)
return dump_response(KnowledgeFSDurableDeletionAcceptedResponse, result), HTTPStatus.ACCEPTED

View File

@ -1,7 +1,7 @@
{
"schemaVersion": 5,
"subtreeTree": "03fdb59383b64f9cd299b89a6e946f000502ade0",
"openapiSha256": "18e3611208c255895cce4a5101bb0ab4466a57389e981a0e70bccacdef96974a",
"subtreeTree": "fed7e05b3624199d02909e2e032fbf82a92c435a",
"openapiSha256": "189c98cd2535829d75b090a70c22dac720a9e9349b8b2cde1602abc31d14f8b8",
"capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7",
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
"productOperationManifestSha256": "a2d5e2b72b87652205d505ff85ce2ca689dbd96a466495883bbf1c4b8a54ff32",

View File

@ -58,6 +58,7 @@ from services.knowledge_fs.product_dto import (
KnowledgeFSGrepResponse,
KnowledgeFSListQuery,
KnowledgeFSListResponse,
KnowledgeFSLogicalDocumentDeletePayload,
KnowledgeFSLogicalDocumentListResponse,
KnowledgeFSLogicalDocumentResponse,
KnowledgeFSOverviewActivityListResponse,
@ -627,7 +628,7 @@ class KnowledgeFSDataFacade:
account_id: str,
control_space_id: str,
document_id: str,
payload: KnowledgeFSDocumentDeletePayload,
payload: KnowledgeFSLogicalDocumentDeletePayload,
idempotency_key: str,
) -> KnowledgeFSDurableDeletionAcceptedResponse:
raw = self._interactive_child(

View File

@ -1150,6 +1150,12 @@ class KnowledgeFSDocumentDeletePayload(BaseModel):
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)
class KnowledgeFSLogicalDocumentDeletePayload(BaseModel):
expected_revision: int = Field(ge=0, alias="expectedRevision")
model_config = ConfigDict(extra="forbid", validate_by_alias=True, validate_by_name=True)
class KnowledgeFSBulkDocumentDeleteItemPayload(KnowledgeFSDocumentDeletePayload):
document_id: str = Field(min_length=1, alias="documentId")
@ -2450,6 +2456,7 @@ __all__ = [
"KnowledgeFSIdempotencyHeader",
"KnowledgeFSJWKResponse",
"KnowledgeFSJWKSResponse",
"KnowledgeFSLogicalDocumentDeletePayload",
"KnowledgeFSLogicalDocumentListResponse",
"KnowledgeFSLogicalDocumentResponse",
"KnowledgeFSMemberBindingPayload",

View File

@ -19,6 +19,7 @@ from controllers.service_api.knowledge_fs import resources as service_resources
from services.knowledge_fs.credential_service import KnowledgeFSServiceCredentialProfile
from services.knowledge_fs.product_dto import (
KnowledgeFSDocumentUploadAcceptedResponse,
KnowledgeFSDurableDeletionAcceptedResponse,
KnowledgeFSSmallFileUploadResponse,
KnowledgeFSSpaceCreatePayload,
)
@ -313,6 +314,59 @@ def test_document_upload_console_bff_reads_only_through_facade_and_returns_accep
]
def test_logical_document_delete_accepts_initial_row_version(
monkeypatch: pytest.MonkeyPatch,
) -> None:
calls: list[dict[str, object]] = []
class Facade:
def delete_logical_document(self, **kwargs):
calls.append(kwargs)
assert kwargs["payload"].expected_revision == 0
return KnowledgeFSDurableDeletionAcceptedResponse.model_validate(
{
"job": {
"checkpoint": "requested",
"createdAt": "2030-01-01T00:00:00Z",
"id": "00000000-0000-4000-8000-000000000001",
"knowledgeSpaceId": "space-1",
"runState": "queued",
"targetId": "00000000-0000-4000-8000-000000000002",
"targetType": "logical_document",
"updatedAt": "2030-01-01T00:00:00Z",
},
"statusUrl": "/deletion-jobs/job-1",
}
)
monkeypatch.setattr(console_resources, "_actor", lambda: ("account-1", "tenant-1"))
monkeypatch.setattr(console_resources, "_console_services", lambda: SimpleNamespace(facade=Facade()))
app = Flask(__name__)
with app.test_request_context(
method="DELETE",
json={"expectedRevision": 0},
headers={"Idempotency-Key": "delete-logical-document-once"},
):
delete = inspect.unwrap(console_resources.KnowledgeFSSpaceLogicalDocumentApi.delete)
response, status = delete(
console_resources.KnowledgeFSSpaceLogicalDocumentApi(),
"control-1",
"document-1",
)
assert status == 202
assert response["job"]["target_type"] == "logical_document"
assert len(calls) == 1
assert {name: value for name, value in calls[0].items() if name != "payload"} == {
"tenant_id": "tenant-1",
"account_id": "account-1",
"control_space_id": "control-1",
"document_id": "document-1",
"idempotency_key": "delete-logical-document-once",
}
def test_small_file_console_bff_maps_oversize_to_413() -> None:
app = Flask(__name__)
with app.test_request_context(

View File

@ -20,6 +20,7 @@ from services.knowledge_fs.product_dto import (
KnowledgeFSGoldenQuestionPayload,
KnowledgeFSGrepQuery,
KnowledgeFSListQuery,
KnowledgeFSLogicalDocumentDeletePayload,
KnowledgeFSProductRerankProfile,
KnowledgeFSProductRetrievalProfile,
KnowledgeFSProductScoreThreshold,
@ -1423,6 +1424,27 @@ def test_advanced_facade_binds_child_resources_parent_space_and_idempotency() ->
]
def test_logical_document_delete_preserves_initial_row_version() -> None:
remote = RecordingRemote()
facade = KnowledgeFSDataFacade(broker=RecordingBroker(), remote=remote) # type: ignore[arg-type]
deletion = facade.delete_logical_document(
tenant_id="tenant-1",
account_id="account-1",
control_space_id="control-1",
document_id="document-1",
payload=KnowledgeFSLogicalDocumentDeletePayload(expected_revision=0),
idempotency_key="delete-logical-once",
)
request = remote.requests[-1]
assert deletion.job.target_type == "logical_document"
assert request.operation_id == "deleteLogicalDocument"
assert request.path == "/knowledge-spaces/space-1/logical-documents/document-1"
assert request.payload == {"expectedRevision": 0}
assert request.headers == (("Idempotency-Key", "delete-logical-once"),)
@pytest.mark.parametrize(
("method_name", "response_name", "operation_id", "specific_kwargs", "child_resource_id"),
[

View File

@ -0,0 +1,60 @@
# Golden-question published-generation evidence resolution
Date: 2026-08-02
## What changed
- Golden-question create and update validation now resolves evidence node ids across retained
publication generations.
- Production bad-case promotion and failed-query promotion use the same durable evidence lookup.
- Direct document-asset evidence ids, candidate permission checks, backing-asset checks, and
required-permission snapshots remain unchanged.
- Added a regression proving that a node from a published generation can be selected from evidence
search and saved as an expected evidence id.
## Why
Evidence matching reads the immutable published projection snapshot, so the returned node ids
belong to a publication generation. Golden-question validation previously called the ordinary
generation-scoped `getMany` repository method without a generation id. That method intentionally
defaults to legacy rows whose `publication_generation_id` is null. A valid published candidate was
therefore misclassified as a direct document-asset id and the save failed with `404 Expected
evidence not found`.
The repository already owns `getManyByIdsAcrossGenerations` specifically for durable evidence
references whose globally unique ids survive publication changes. Using that lookup makes the
search and save boundaries agree without weakening authorization.
## Performance and reliability
- Node lookup remains one bounded batch for all selected evidence ids; there is no per-node query.
- The existing maximum of 50 expected evidence ids still bounds the cross-generation lookup.
- Backing document assets are still checked before persistence and malformed or unauthorized
permission scopes continue to fail closed.
- Published and retained generations are addressed only by globally unique node ids; no
unbounded generation scan is introduced.
## Verification
- TDD red phase reproduced the failure: a published-generation node was ignored by the legacy
`getMany` path and the expected permission scope was empty.
- Focused golden-question and failed-query handler and gateway tests passed: 4 files, 38 tests.
- KnowledgeFS API typecheck passed.
- `pnpm check` passed, including workspace tests, coverage gates, evaluations, contract checks,
migration checks, Compose validation, and smoke-test definitions.
- `pnpm build` passed for all 12 packages.
- The four changed TypeScript source/test files pass focused Biome checks, and `git diff --check`
passed.
- The Dify KnowledgeFS contract lock was intentionally refreshed and its `--check` command passed.
This fix does not add a Golden Question OpenAPI or Capability v2 contract change; the staged
OpenAPI digest update belongs to the separate logical-document deletion fix.
## Known baseline
- Full `pnpm lint` remains blocked by 10 pre-existing findings in unchanged Admin, fixture,
OpenAPI, and generated capability files. No finding is in a file changed by this fix.
## Rollout
- The KnowledgeFS API must be rebuilt and redeployed before retrying the Console PATCH request.
- The production request supplied for diagnosis was not replayed because it mutates user data.

View File

@ -0,0 +1,66 @@
# Logical document initial-revision deletion
Date: 2026-08-02
## What changed
- Split logical-document deletion from document-asset deletion at both the Dify Console BFF and
KnowledgeFS request-contract boundaries.
- Logical-document deletion now accepts a non-negative row version, including the initial
`expectedRevision: 0` value returned by the logical-document API.
- Document-asset deletion remains strictly positive; the shared asset payload was not relaxed.
- Updated the durable deletion repository guard so compare-and-swap deletion can target an
initial logical-document row version of zero.
- Regenerated the Dify Console TypeScript contract and added controller, facade, handler,
repository, and generated-contract regression coverage.
## Why
Logical documents are created with `row_version = 0`, and their response contract already exposes
zero as a valid row version. The delete route incorrectly reused the document-asset payload, whose
version starts at one. As a result, a correct delete request for a newly created logical document
was rejected first by Dify's Pydantic validation and, if it reached KnowledgeFS, by the shared Zod
and repository positive-integer guards.
This change gives the two independently versioned resources separate payload contracts and keeps
optimistic concurrency intact. It does not bypass or remove the expected-revision check.
## Performance and reliability
- The deletion path, transaction count, SQL compare-and-swap predicate, and durable job behavior
are unchanged.
- Validation remains constant time and no additional remote calls or database queries were added.
- A stale zero revision still conflicts after the logical document advances to a later row version.
- The idempotency-key requirement remains unchanged.
## Verification
- TDD red phase reproduced rejection at all affected boundaries:
- KnowledgeFS route validation rejected logical `expectedRevision: 0`.
- PostgreSQL and TiDB repository tests rejected a zero logical row version.
- Dify Console payload validation rejected a zero logical row version.
- The generated Console client contract rejected a zero logical row version.
- Focused KnowledgeFS handler and repository tests passed: 2 files, 85 tests.
- KnowledgeFS API typecheck passed.
- `pnpm check` and `pnpm build` passed.
- Dify controller and data-facade tests passed: 104 tests.
- Targeted Dify Ruff, Pyrefly, and Mypy checks passed.
- Generated Console logical-document deletion contract smoke test passed.
- Generated Console contract typecheck passed.
- Contract generation and lock checks passed.
- Targeted Python Ruff and KnowledgeFS Biome checks passed.
- `git diff --check` passed.
## Known risks / follow-up
- The Dify API and KnowledgeFS API must both be deployed before retrying the production request;
deploying only one side leaves another rejecting boundary in place.
- Full `pnpm lint` remains blocked by 10 pre-existing findings in unchanged Admin, fixture,
OpenAPI, and generated capability files. All changed KnowledgeFS TypeScript files pass focused
Biome checks.
- The standalone KnowledgeFS API coverage command executes all tests successfully but the existing
repository-wide branch result is 89.94%, below its 90% threshold. This change adds no runtime
branches, and the changed request-schema file reports 100% coverage.
- The production request supplied for diagnosis was not replayed because it was destructive.
- Temporary progress documents were not recreated; this change record is the traceability source
for this fix.

View File

@ -85,7 +85,7 @@ describe("durable deletion handlers", () => {
const response = await app.request(
`/knowledge-spaces/${SPACE_ID}/logical-documents/${DOCUMENT_ID}`,
{
body: JSON.stringify({ expectedRevision: 4 }),
body: JSON.stringify({ expectedRevision: 0 }),
headers: requestHeaders(),
method: "DELETE",
},
@ -101,7 +101,7 @@ describe("durable deletion handlers", () => {
expect.objectContaining({
callerKind: "interactive",
documentId: DOCUMENT_ID,
expectedRevision: 4,
expectedRevision: 0,
idempotencyKey: "delete-space-0001",
knowledgeSpaceId: SPACE_ID,
}),
@ -109,6 +109,19 @@ describe("durable deletion handlers", () => {
expect(service.requestDocumentDeletion).not.toHaveBeenCalled();
});
it("keeps document-asset deletion versions strictly positive", async () => {
const service = serviceStub();
const app = testApp(service);
const response = await app.request(`/knowledge-spaces/${SPACE_ID}/documents/${DOCUMENT_ID}`, {
body: JSON.stringify({ expectedRevision: 0 }),
headers: requestHeaders(),
method: "DELETE",
});
expect(response.status).toBe(400);
expect(service.requestDocumentDeletion).not.toHaveBeenCalled();
});
it("fails closed with 503 when the durable service is not configured", async () => {
const app = testApp(undefined);
const response = await app.request(

View File

@ -6,6 +6,7 @@ import type {
DeleteDocumentParams,
DeleteKnowledgeSpaceBody,
DeleteKnowledgeSpaceParams,
DeleteLogicalDocumentBody,
DeleteSourceBody,
DeleteSourceParams,
DeleteSourceQuery,
@ -176,7 +177,7 @@ export function registerDurableDeletionHandlers({
return unavailable(context);
}
const params = context.req.valid("param") as DeleteDocumentParams;
const body = context.req.valid("json") as DeleteDocumentBody;
const body = context.req.valid("json") as DeleteLogicalDocumentBody;
const headers = context.req.valid("header") as DurableDeletionIdempotencyHeaders;
try {
const accepted = await service.requestLogicalDocumentDeletion({

View File

@ -556,7 +556,7 @@ describe.each(["postgres", "tidb"] as const)(
? [
jobRow({
idempotency_key: "delete-logical-a",
target_revision: 4,
target_revision: 0,
target_type: "logical_document",
}),
]
@ -582,7 +582,7 @@ describe.each(["postgres", "tidb"] as const)(
rows: [
{
active_revision: null,
row_version: 4,
row_version: 0,
source_id: null,
status: "failed",
},
@ -638,7 +638,7 @@ describe.each(["postgres", "tidb"] as const)(
accessChannel: "interactive",
createdAt,
documentId: targetId,
expectedDocumentRowVersion: 4,
expectedDocumentRowVersion: 0,
idempotencyKey: "delete-logical-a",
knowledgeSpaceId,
permissionSnapshotId,
@ -648,7 +648,7 @@ describe.each(["postgres", "tidb"] as const)(
}),
).resolves.toMatchObject({
created: true,
job: { targetRevision: 4, targetType: "logical_document" },
job: { targetRevision: 0, targetType: "logical_document" },
});
const logicalUpdate = calls.find(
@ -665,7 +665,7 @@ describe.each(["postgres", "tidb"] as const)(
tenantId,
knowledgeSpaceId,
targetId,
4,
0,
]);
expect(assetUpdate?.inTransaction).toBe(true);
expect(assetUpdate?.sql).toContain("owned_revision");

View File

@ -609,7 +609,7 @@ export function createDatabaseDurableDeletionRepository({
: target.type === "logical_document"
? {
deleteMode: "cascade" as const,
expectedRevision: positiveInteger(
expectedRevision: nonnegativeInteger(
(input as RequestLogicalDocumentDeletionInput).expectedDocumentRowVersion,
"expectedDocumentRowVersion",
),

View File

@ -1,6 +1,7 @@
import { z } from "@hono/zod-openapi";
const ExpectedRevisionSchema = z.number().int().positive();
const LogicalDocumentExpectedRevisionSchema = z.number().int().nonnegative();
export const DurableDeletionIdempotencyHeadersSchema = z
.object({
@ -51,6 +52,12 @@ export const DeleteDocumentBodySchema = z
})
.strict();
export const DeleteLogicalDocumentBodySchema = z
.object({
expectedRevision: LogicalDocumentExpectedRevisionSchema,
})
.strict();
export const BulkDeleteDocumentsBodySchema = z
.object({
documents: z
@ -72,6 +79,7 @@ export type DeleteSourceBody = z.infer<typeof DeleteSourceBodySchema>;
export type DeleteSourceParams = z.infer<typeof DeleteSourceParamsSchema>;
export type DeleteSourceQuery = z.infer<typeof DurableDeleteSourceQuerySchema>;
export type DeleteDocumentBody = z.infer<typeof DeleteDocumentBodySchema>;
export type DeleteLogicalDocumentBody = z.infer<typeof DeleteLogicalDocumentBodySchema>;
export type DeleteDocumentParams = z.infer<typeof DeleteDocumentParamsSchema>;
export type BulkDeleteDocumentsBody = z.infer<typeof BulkDeleteDocumentsBodySchema>;
export type DurableDeletionIdempotencyHeaders = z.infer<

View File

@ -6,6 +6,7 @@ import {
DeleteDocumentParamsSchema,
DeleteKnowledgeSpaceBodySchema,
DeleteKnowledgeSpaceParamsSchema,
DeleteLogicalDocumentBodySchema,
DeleteSourceBodySchema,
DeleteSourceParamsSchema,
DurableDeleteSourceQuerySchema,
@ -129,7 +130,7 @@ export const requestLogicalDocumentDeletionRoute = createRoute({
path: "/knowledge-spaces/{id}/logical-documents/{documentId}",
request: {
body: {
content: { "application/json": { schema: DeleteDocumentBodySchema } },
content: { "application/json": { schema: DeleteLogicalDocumentBodySchema } },
required: true,
},
headers: DurableDeletionIdempotencyHeadersSchema,

View File

@ -284,7 +284,7 @@ function failedQueryFixture(options: FailedQueryFixtureOptions = {}) {
...(options.triageRunner === null
? {}
: { failedQueryTriageRunner: { run: triageRun } as never }),
nodes: { getMany: vi.fn(async () => []) } as never,
nodes: { getManyByIdsAcrossGenerations: vi.fn(async () => []) } as never,
now: () => "2026-07-14T12:00:00.000Z",
spaces: {
get: vi.fn(async () => (options.space === undefined ? { id: SPACE_ID } : options.space)),

View File

@ -34,7 +34,7 @@ export interface RegisterFailedQueryHandlersOptions {
readonly failedQueries: FailedQueryRepository;
readonly failedQueryTriageRunner?: FailedQueryTriageRunner | undefined;
readonly now?: () => string;
readonly nodes: Pick<KnowledgeNodeRepository, "getMany">;
readonly nodes: Pick<KnowledgeNodeRepository, "getManyByIdsAcrossGenerations">;
readonly spaces: KnowledgeSpaceRepository;
}

View File

@ -231,6 +231,33 @@ describe("golden-question handler branch coverage", () => {
});
describe("golden-question evidence permission scope branches", () => {
it("resolves published-generation evidence nodes by their durable ids", async () => {
const getMany = vi.fn(async () => []);
const getManyByIdsAcrossGenerations = vi.fn(async () => [
{
documentAssetId: ASSET_ID,
id: NODE_ID,
permissionScope: ["team:a"],
publicationGenerationId: "018f0d60-7a49-7cc2-9c1b-5b36f18f2c46",
},
]);
await expect(
goldenQuestionEvidencePermissionScope({
assets: { get: vi.fn(async () => ({ metadata: {} })) } as never,
candidateGrants: ["team:a"],
expectedEvidenceIds: [NODE_ID],
knowledgeSpaceId: SPACE_ID,
nodes: { getMany, getManyByIdsAcrossGenerations } as never,
}),
).resolves.toEqual(["team:a"]);
expect(getManyByIdsAcrossGenerations).toHaveBeenCalledWith({
ids: [NODE_ID],
knowledgeSpaceId: SPACE_ID,
});
expect(getMany).not.toHaveBeenCalled();
});
it("handles empty, duplicate, direct-asset, and node-backed evidence", async () => {
await expect(evidenceScope({ expectedEvidenceIds: [] })).resolves.toEqual([]);
await expect(evidenceScope({ expectedEvidenceIds: [ASSET_ID, ASSET_ID] })).resolves.toBeNull();
@ -353,7 +380,7 @@ function goldenFixture(options: GoldenFixtureOptions = {}) {
get: vi.fn(async () => (options.asset === undefined ? { metadata: {} } : options.asset)),
} as never,
authorization: { authorize } as never,
nodes: { getMany: vi.fn(async () => []) } as never,
nodes: { getManyByIdsAcrossGenerations: vi.fn(async () => []) } as never,
now: () => "2026-07-14T12:00:00.000Z",
questions: questions as never,
spaces: {
@ -414,7 +441,7 @@ function evidenceScope(options: {
candidateGrants: options.candidateGrants ?? [],
expectedEvidenceIds: options.expectedEvidenceIds,
knowledgeSpaceId: SPACE_ID,
nodes: { getMany: vi.fn(async () => options.nodes ?? []) } as never,
nodes: { getManyByIdsAcrossGenerations: vi.fn(async () => options.nodes ?? []) } as never,
});
}

View File

@ -62,7 +62,7 @@ export interface RegisterGoldenQuestionHandlersOptions {
readonly assets: Pick<DocumentAssetRepository, "get">;
readonly authorization: KnowledgeSpaceAuthorizationGuard;
readonly evidenceMatcher?: GoldenQuestionEvidenceMatcher | undefined;
readonly nodes: Pick<KnowledgeNodeRepository, "getMany">;
readonly nodes: Pick<KnowledgeNodeRepository, "getManyByIdsAcrossGenerations">;
readonly now: () => string;
readonly questions: GoldenQuestionRepository;
readonly spaces: KnowledgeSpaceRepository;
@ -726,7 +726,7 @@ async function productionBadCaseEvidencePermissionScope(input: {
readonly assets: Pick<DocumentAssetRepository, "get">;
readonly bundle: EvidenceBundle | null;
readonly knowledgeSpaceId: string;
readonly nodes: Pick<KnowledgeNodeRepository, "getMany">;
readonly nodes: Pick<KnowledgeNodeRepository, "getManyByIdsAcrossGenerations">;
readonly permissionScopes: readonly string[];
}): Promise<readonly string[] | null> {
if (!input.bundle) {
@ -746,7 +746,7 @@ async function productionBadCaseEvidencePermissionScope(input: {
.map((missing) => missing.expectedEvidenceId)
.filter((id): id is string => id !== undefined),
);
const referencedNodes = await input.nodes.getMany({
const referencedNodes = await input.nodes.getManyByIdsAcrossGenerations({
ids: uniqueStrings([...requiredNodeIds, ...optionalMissingNodeIds]),
knowledgeSpaceId: input.knowledgeSpaceId,
});
@ -804,12 +804,12 @@ export async function goldenQuestionEvidencePermissionScope(input: {
readonly candidateGrants: readonly string[];
readonly expectedEvidenceIds: readonly string[];
readonly knowledgeSpaceId: string;
readonly nodes: Pick<KnowledgeNodeRepository, "getMany">;
readonly nodes: Pick<KnowledgeNodeRepository, "getManyByIdsAcrossGenerations">;
}): Promise<readonly string[] | null> {
const evidenceIds = uniqueStrings(input.expectedEvidenceIds);
if (evidenceIds.length !== input.expectedEvidenceIds.length) return null;
if (evidenceIds.length === 0) return [];
const nodes = await input.nodes.getMany({
const nodes = await input.nodes.getManyByIdsAcrossGenerations({
ids: evidenceIds,
knowledgeSpaceId: input.knowledgeSpaceId,
});

View File

@ -378,6 +378,10 @@ export type KnowledgeFsLogicalDocumentListResponse = {
next_cursor?: string | null
}
export type KnowledgeFsLogicalDocumentDeletePayload = {
expectedRevision: number
}
export type KnowledgeFsMembersReplacePayload = {
members: Array<KnowledgeFsMemberBindingPayload>
}
@ -2255,7 +2259,7 @@ export type GetKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsResponse =
GetKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsResponses[keyof GetKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsResponses]
export type DeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdData = {
body: KnowledgeFsDocumentDeletePayload
body: KnowledgeFsLogicalDocumentDeletePayload
headers: {
'Idempotency-Key': string
}

View File

@ -269,6 +269,13 @@ export const zKnowledgeFsDocumentCompilationJobResponse = z.object({
version: z.int().gte(1),
})
/**
* KnowledgeFSLogicalDocumentDeletePayload
*/
export const zKnowledgeFsLogicalDocumentDeletePayload = z.object({
expectedRevision: z.int().gte(0),
})
/**
* KnowledgeFSBadCaseCreatePayload
*/
@ -2577,7 +2584,7 @@ export const zGetKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsResponse =
zKnowledgeFsLogicalDocumentListResponse
export const zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdBody =
zKnowledgeFsDocumentDeletePayload
zKnowledgeFsLogicalDocumentDeletePayload
export const zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdHeaders = z.object(
{

View File

@ -3,6 +3,7 @@ import { logicalDocuments } from './generated/api/console/knowledge-fs/orpc.gen'
import {
zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdBody,
zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdHeaders,
zKnowledgeFsDocumentDeletePayload,
} from './generated/api/console/knowledge-fs/zod.gen'
describe('generated KnowledgeFS logical document deletion contract', () => {
@ -17,6 +18,11 @@ describe('generated KnowledgeFS logical document deletion contract', () => {
zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdBody.safeParse({
expectedRevision: 0,
}).success,
).toBe(true)
expect(
zKnowledgeFsDocumentDeletePayload.safeParse({
expectedRevision: 0,
}).success,
).toBe(false)
expect(
zDeleteKnowledgeFsSpacesByControlSpaceIdLogicalDocumentsByDocumentIdHeaders.safeParse({