{"record":{"id":"cf00bb4d69a0b39b","repo":"toeverything/AFFiNE","slug":"app-config-paths-must-not-overlap-overlappingke","errorCode":null,"errorMessage":"App config paths must not overlap: ${overlappingKey} and ${key}","messagePattern":"App config paths must not overlap: (.+?) and (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/backend/server/src/models/config.ts","lineNumber":34,"sourceCode":"  @Transactional()\n  async save(user: string, updates: Array<{ key: string; value: any }>) {\n    await this.db\n      .$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'app-config-paths'}, 0))`;\n    const existing = await this.db.appConfig.findMany({\n      select: { id: true },\n    });\n    const updateKeys = updates.map(update => update.key);\n    for (const [index, key] of updateKeys.entries()) {\n      const overlappingKey = [\n        ...existing.map(config => config.id),\n        ...updateKeys.slice(0, index),\n      ].find(\n        candidate =>\n          candidate !== key &&\n          (candidate.startsWith(`${key}.`) || key.startsWith(`${candidate}.`))\n      );\n      if (overlappingKey) {\n        throw new Error(\n          `App config paths must not overlap: ${overlappingKey} and ${key}`\n        );\n      }\n    }\n\n    return await Promise.allSettled(\n      updates.map(async update => {\n        return this.db.appConfig.upsert({\n          where: { id: update.key },\n          update: { value: update.value, lastUpdatedBy: user },\n          create: { id: update.key, value: update.value, lastUpdatedBy: user },\n        });\n      })\n    );\n  }\n\n  async get(key: string) {\n    return await this.db.appConfig.findUnique({ where: { id: key } });","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/models/config.ts#L16-L52","documentation":"Plain Error thrown by AppConfigModel (packages/backend/server/src/models/config.ts:34) during a bulk app-config update. For each key in updates it scans existing config ids plus previously-seen update keys for a hierarchical overlap: one is a prefix of the other via startsWith('x.'). The guard prevents two configs from shadowing each other in a dotted namespace (e.g. 'a' vs 'a.b').","triggerScenarios":"Posting an app-config update batch where one key is an ancestor/descendant of another. Triggers when candidate.startsWith(`${key}.`) OR key.startsWith(`${candidate}.`) for any earlier update key or any existing appConfig.id. Example: updating [{key:'feature.x'}, {key:'feature'}] in one call, or adding 'feature' when 'feature.x' already exists.","commonSituations":"Migrating a flat key to a namespace (or vice versa) without first deleting children; bulk-importing configs with mixed nesting depths; UI saving both a parent toggle and a child setting in the same payload; renaming a config namespace.","solutions":["Split the batch so ancestor and descendant are never updated together; update leaves first, then parents.","Delete the colliding existing key (db.appConfig.delete) before inserting its ancestor/descendant.","Flatten your config schema so keys do not share prefixes, or adopt a non-dotted separator.","Inspect existing ids first: const existing = await db.appConfig.findMany({ select: { id: true } }); and diff against your proposed keys."],"exampleFix":"// before\nawait configModel.setMany(user, [\n  { key: 'feature', value: 'on' },\n  { key: 'feature.flag', value: 'off' }, // overlap -> throws\n]);\n// after\nawait configModel.setMany(user, [{ key: 'feature.flag', value: 'off' }]);\nawait db.appConfig.deleteMany({ where: { id: { startsWith: 'feature.' } } });\nawait configModel.setMany(user, [{ key: 'feature', value: 'on' }]);","handlingStrategy":"validation","validationCode":"function findOverlap(keys: string[], existing: string[]): [string, string] | null {\n  for (const key of keys) {\n    const hit = [...existing, ...keys.filter(k => k !== key)].find(c =>\n      c !== key && (c.startsWith(`${key}.`) || key.startsWith(`${c}.`))\n    );\n    if (hit) return [hit, key];\n  }\n  return null;\n}\nconst existingIds = (await db.appConfig.findMany({ select: { id: true } })).map(r => r.id);\nif (findOverlap(updates.map(u => u.key), existingIds)) throw new Error('overlap');","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never update a parent key and its dotted descendants in the same batch.","Delete child keys before introducing an ancestor of them.","Avoid dotted hierarchical config schemas unless you need prefix semantics."],"tags":["app-config","config","validation","nested-keys"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}