{"record":{"id":"f0ccc1d2c826f5df","repo":"toeverything/AFFiNE","slug":"doc-is-not-public","errorCode":"doc_is_not_public","errorMessage":"Doc is not public.","messagePattern":"Doc is not public\\.","errorType":"exception","errorClass":"DocIsNotPublic","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/models/doc.ts","lineNumber":609,"sourceCode":"  /**\n   * Publish a doc as public.\n   */\n  async publish(\n    workspaceId: string,\n    docId: string,\n    mode: PublicDocMode = PublicDocMode.Page\n  ) {\n    return await this.upsertMeta(workspaceId, docId, {\n      public: true,\n      mode,\n      publishedAt: new Date(),\n    });\n  }\n\n  @Transactional()\n  async unpublish(workspaceId: string, docId: string) {\n    if (!(await this.isPublic(workspaceId, docId))) {\n      throw new DocIsNotPublic();\n    }\n\n    return await this.upsertMeta(workspaceId, docId, {\n      public: false,\n      publishedAt: null,\n    });\n  }\n\n  /**\n   * Check if the doc is public.\n   */\n  async isPublic(workspaceId: string, docId: string) {\n    const policy = await this.db.docAccessPolicy.findUnique({\n      where: { workspaceId_docId: { workspaceId, docId } },\n      select: { visibility: true, publicRole: true },\n    });\n    return policy?.visibility === 'public' && policy.publicRole === 'external';\n  }","sourceCodeStart":591,"sourceCodeEnd":627,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/models/doc.ts#L591-L627","documentation":"DocIsNotPublic, thrown by DocModel.unpublish() (doc.ts:590-601). unpublish is a guarded state transition: it first awaits isPublic(workspaceId, docId) (which reads the doc meta's public flag) and throws when the doc was never published or is already unpublished, before upserting meta with public: false and publishedAt: null.","triggerScenarios":"unpublish(workspaceId, docId) on a doc whose meta does not have public = true - never published, already unpublished (double call), or no meta row yet.","commonSituations":"Retried unpublish requests (double-click, network retry); UI showing a stale 'Published' badge; cleanup routines unpublishing every doc indiscriminately.","solutions":["Check the published state first (isPublic) and skip the call when not public - make unpublish idempotent at the call site","Catch DocIsNotPublic and treat it as success when you only need the end state 'not published'","Refresh the doc's public state in the UI when the badge disagrees with the server"],"exampleFix":"// before\nawait doc.unpublish(workspaceId, docId);\n\n// after - idempotent unpublish\nif (await doc.isPublic(workspaceId, docId)) {\n  await doc.unpublish(workspaceId, docId);\n}","handlingStrategy":"validation","validationCode":"if (await doc.isPublic(workspaceId, docId)) {\n  await doc.unpublish(workspaceId, docId);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await doc.unpublish(workspaceId, docId);\n} catch (e) {\n  if (e instanceof DocIsNotPublic) return; // already unpublished - treat as success\n  throw e;\n}","preventionTips":["Make unpublish idempotent at the call site with an isPublic pre-check","Refresh publish state after actions that change it","Guard double-clicks and network retries in the UI"],"tags":["doc","publish","state-transition","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}