{"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":"error_code","errorClass":"DocIsNotPublic","httpStatus":400,"severity":"warning","filePath":"packages/backend/server/src/models/doc.ts","lineNumber":597,"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":579,"sourceCodeEnd":615,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/doc.ts#L579-L615","documentation":"DocIsNotPublic (bad_request / doc_is_not_public) thrown by DocModel.unpublish (packages/backend/server/src/models/doc.ts:597). unpublish first calls isPublic(workspaceId, docId); if the doc is not currently published, there is nothing to unpublish and the guard throws before upsertMeta sets public:false / publishedAt:null.","triggerScenarios":"Calling unpublish(workspaceId, docId) on a doc whose meta does not have public:true. isPublic returns false (no public meta row or public=false), so the operation aborts.","commonSituations":"User clicks 'Unpublish' on a doc that was never published or was already unpublished; double-submit of the unpublish action; UI state desync showing a doc as public when it is not; race between two unpublish calls.","solutions":["Guard the call: if (!(await docModel.isPublic(ws, doc))) return; before unpublish.","Make the unpublish control conditional on the doc's published state in the UI.","Catch doc_is_not_public and treat as success (idempotent unpublish).","Refresh doc meta before showing the unpublish button."],"exampleFix":"// before\nawait docModel.unpublish(workspaceId, docId);\n// after\nif (await docModel.isPublic(workspaceId, docId)) {\n  await docModel.unpublish(workspaceId, docId);\n}","handlingStrategy":"validation","validationCode":"if (!(await docModel.isPublic(workspaceId, docId))) {\n  return; // already private — nothing to unpublish\n}\nawait docModel.unpublish(workspaceId, docId);","typeGuard":"const isPublicDoc = async (ws: string, doc: string): Promise<boolean> =>\n  await docModel.isPublic(ws, doc);","tryCatchPattern":"try {\n  await docModel.unpublish(workspaceId, docId);\n} catch (e) {\n  if (e instanceof UserFriendlyError && e.code === 'doc_is_not_public') return; // idempotent\n  throw e;\n}","preventionTips":["Make the 'Unpublish' control conditional on the doc's published state.","Treat unpublish as idempotent — a not-public doc is the goal state.","Refresh doc meta before toggling publish state."],"tags":["doc","publish","public","idempotent"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}