{"version":3,"sources":["../../../src/config/orderable/index.ts"],"sourcesContent":["import { status as httpStatus } from 'http-status'\n\nimport type { BeforeChangeHook, CollectionConfig } from '../../collections/config/types.js'\nimport type { Config } from '../../config/types.js'\nimport type { Field, TextField } from '../../fields/config/types.js'\nimport type { Endpoint, PayloadHandler, SanitizedConfig } from '../types.js'\n\nimport { executeAccess } from '../../auth/executeAccess.js'\nimport { APIError } from '../../errors/index.js'\nimport { sanitizeField } from '../../fields/config/sanitize.js'\nimport { combineWhereConstraints } from '../../utilities/combineWhereConstraints.js'\nimport { commitTransaction } from '../../utilities/commitTransaction.js'\nimport { initTransaction } from '../../utilities/initTransaction.js'\nimport { killTransaction } from '../../utilities/killTransaction.js'\nimport { generateKeyBetween, generateNKeysBetween } from './fractional-indexing.js'\nimport { getJoinScopeContext } from './utils/getJoinScopeContext.js'\nimport { getJoinScopeWhereFromDocData } from './utils/getJoinScopeWhereFromDocData.js'\nimport { resolvePendingTargetKey } from './utils/resolvePendingTargetKey.js'\n\nexport const addOrderableFieldsAndHook = async (\n  collection: CollectionConfig,\n  config: Config,\n  orderableFieldNames: string[],\n  joinFieldPathsByCollection?: Map<string, Map<string, string>>,\n) => {\n  // 1. Add fields\n  for (const orderableFieldName of orderableFieldNames) {\n    const orderField: TextField = {\n      name: orderableFieldName,\n      type: 'text',\n      admin: {\n        disableBulkEdit: true,\n        disabled: true,\n        disableGroupBy: true,\n        disableListColumn: true,\n        disableListFilter: true,\n        hidden: true,\n        readOnly: true,\n      },\n      hooks: {\n        beforeDuplicate: [\n          ({ siblingData }) => {\n            delete siblingData[orderableFieldName]\n          },\n        ],\n      },\n      index: true,\n    }\n\n    // Sanitize the field using the standard sanitization logic\n    await sanitizeField({\n      collectionConfig: collection,\n      config,\n      existingFieldNames: new Set(),\n      field: orderField,\n      index: 0,\n      isTopLevelField: true,\n      joinPath: '',\n      parentIndexPath: '',\n      parentIsLocalized: false,\n      parentSchemaPath: '',\n      requireFieldLevelRichTextEditor: false,\n      validRelationships: null,\n    })\n\n    collection.fields.unshift(orderField)\n  }\n\n  // 2. Add hook\n  if (!collection.hooks) {\n    collection.hooks = {}\n  }\n  if (!collection.hooks.beforeChange) {\n    collection.hooks.beforeChange = []\n  }\n\n  const orderBeforeChangeHook: BeforeChangeHook = async ({ data, originalDoc, req }) => {\n    for (const orderableFieldName of orderableFieldNames) {\n      if (!data[orderableFieldName] && !originalDoc?.[orderableFieldName]) {\n        const joinScopeWhere = getJoinScopeWhereFromDocData({\n          collectionSlug: collection.slug,\n          data,\n          joinFieldPathsByCollection,\n          orderableFieldName,\n          originalDoc,\n        })\n\n        const lastDoc = await req.payload.find({\n          collection: collection.slug,\n          depth: 0,\n          limit: 1,\n          pagination: false,\n          req,\n          select: { [orderableFieldName]: true },\n          sort: `-${orderableFieldName}`,\n          where: combineWhereConstraints([\n            {\n              [orderableFieldName]: {\n                exists: true,\n              },\n            },\n            joinScopeWhere ?? undefined,\n          ]),\n        })\n\n        const lastOrderValue = lastDoc.docs[0]?.[orderableFieldName] || null\n        data[orderableFieldName] = generateKeyBetween(lastOrderValue, null)\n      }\n    }\n\n    return data\n  }\n\n  collection.hooks.beforeChange.push(orderBeforeChangeHook)\n}\n\n/**\n * The body of the reorder endpoint.\n * @internal\n */\nexport type OrderableEndpointBody = {\n  collectionSlug: string\n  docsToMove: string[]\n  newKeyWillBe: 'greater' | 'less'\n  orderableFieldName: string\n  target: {\n    id: string\n    key: string\n  }\n}\n\nexport const addOrderableEndpoint = (\n  config: SanitizedConfig,\n  joinFieldPathsByCollection: Map<string, Map<string, string>>,\n) => {\n  // 3. Add endpoint\n  const reorderHandler: PayloadHandler = async (req) => {\n    const body = (await req.json?.()) as OrderableEndpointBody\n\n    const { collectionSlug, docsToMove, newKeyWillBe, orderableFieldName, target } = body\n\n    if (!Array.isArray(docsToMove) || docsToMove.length === 0) {\n      return new Response(JSON.stringify({ error: 'docsToMove must be a non-empty array' }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 400,\n      })\n    }\n    if (newKeyWillBe !== 'greater' && newKeyWillBe !== 'less') {\n      return new Response(JSON.stringify({ error: 'newKeyWillBe must be \"greater\" or \"less\"' }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 400,\n      })\n    }\n    const collection = config.collections.find((c) => c.slug === collectionSlug)\n    if (!collection) {\n      return new Response(JSON.stringify({ error: `Collection ${collectionSlug} not found` }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 400,\n      })\n    }\n    if (typeof orderableFieldName !== 'string') {\n      return new Response(JSON.stringify({ error: 'orderableFieldName must be a string' }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 400,\n      })\n    }\n\n    const { joinScopeWhere, targetDoc } = await getJoinScopeContext({\n      collectionSlug: collection.slug,\n      joinFieldPathsByCollection,\n      orderableFieldName,\n      req,\n      target,\n    })\n\n    // Prevent reordering if user doesn't have editing permissions\n    if (collection.access?.update) {\n      await executeAccess(\n        {\n          // Currently only one doc can be moved at a time. We should review this if we want to allow\n          // multiple docs to be moved at once in the future.\n          id: docsToMove[0],\n          data: {},\n          req,\n        },\n        collection.access.update,\n      )\n    }\n    /**\n     * If there is no target.key, we can assume the user enabled `orderable`\n     * on a collection with existing documents, and that this is the first\n     * time they tried to reorder them. Therefore, we perform a one-time\n     * migration by setting the key value for all documents. We do this\n     * instead of enforcing `required` and `unique` at the database schema\n     * level, so that users don't have to run a migration when they enable\n     * `orderable` on a collection with existing documents.\n     */\n    if (!target.key) {\n      const { docs } = await req.payload.find({\n        collection: collection.slug,\n        depth: 0,\n        limit: 0,\n        req,\n        select: { [orderableFieldName]: true },\n        where: combineWhereConstraints([\n          {\n            [orderableFieldName]: {\n              exists: false,\n            },\n          },\n          joinScopeWhere ?? undefined,\n        ]),\n      })\n      await initTransaction(req)\n      // We cannot update all documents in a single operation with `payload.update`,\n      // because they would all end up with the same order key (`a0`).\n      try {\n        for (const doc of docs) {\n          await req.payload.update({\n            id: doc.id,\n            collection: collection.slug,\n            data: {\n              // no data needed since the order hooks will handle this\n            },\n            depth: 0,\n            req,\n          })\n          await commitTransaction(req)\n        }\n      } catch (e) {\n        await killTransaction(req)\n        if (e instanceof Error) {\n          throw new APIError(e.message, httpStatus.INTERNAL_SERVER_ERROR)\n        }\n      }\n\n      return new Response(JSON.stringify({ message: 'initial migration', success: true }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 200,\n      })\n    }\n\n    if (\n      typeof target !== 'object' ||\n      typeof target.id === 'undefined' ||\n      typeof target.key !== 'string'\n    ) {\n      return new Response(JSON.stringify({ error: 'target must be an object with id' }), {\n        headers: { 'Content-Type': 'application/json' },\n        status: 400,\n      })\n    }\n\n    const targetId = target.id\n    const targetKey = await resolvePendingTargetKey({\n      collectionSlug: collection.slug,\n      orderableFieldName,\n      req,\n      targetDoc,\n      targetID: targetId,\n      targetKey: target.key,\n    })\n\n    // The reason the endpoint does not receive this docId as an argument is that there\n    // are situations where the user may not see or know what the next or previous one is. For\n    // example, access control restrictions, if docBefore is the last one on the page, etc.\n    const adjacentDoc = await req.payload.find({\n      collection: collection.slug,\n      depth: 0,\n      limit: 1,\n      pagination: false,\n      select: { [orderableFieldName]: true },\n      sort: newKeyWillBe === 'greater' ? orderableFieldName : `-${orderableFieldName}`,\n      where: combineWhereConstraints([\n        {\n          [orderableFieldName]: {\n            [newKeyWillBe === 'greater' ? 'greater_than' : 'less_than']: targetKey,\n          },\n        },\n        joinScopeWhere ?? undefined,\n      ]),\n    })\n    const adjacentDocKey = adjacentDoc.docs?.[0]?.[orderableFieldName] || null\n\n    // Currently N (= docsToMove.length) is always 1. Maybe in the future we will\n    // allow dragging and reordering multiple documents at once via the UI.\n    const orderValues =\n      newKeyWillBe === 'greater'\n        ? generateNKeysBetween(targetKey, adjacentDocKey, docsToMove.length)\n        : generateNKeysBetween(adjacentDocKey, targetKey, docsToMove.length)\n\n    // Update each document with its new order value\n    for (const [index, id] of docsToMove.entries()) {\n      await req.payload.update({\n        id,\n        collection: collection.slug,\n        data: {\n          [orderableFieldName]: orderValues[index],\n        },\n        depth: 0,\n        req,\n      })\n    }\n\n    return new Response(JSON.stringify({ orderValues, success: true }), {\n      headers: { 'Content-Type': 'application/json' },\n      status: 200,\n    })\n  }\n\n  const reorderEndpoint: Endpoint = {\n    handler: reorderHandler,\n    method: 'post',\n    path: '/reorder',\n  }\n\n  if (!config.endpoints) {\n    config.endpoints = []\n  }\n\n  config.endpoints.push(reorderEndpoint)\n}\n"],"names":["status","httpStatus","executeAccess","APIError","sanitizeField","combineWhereConstraints","commitTransaction","initTransaction","killTransaction","generateKeyBetween","generateNKeysBetween","getJoinScopeContext","getJoinScopeWhereFromDocData","resolvePendingTargetKey","addOrderableFieldsAndHook","collection","config","orderableFieldNames","joinFieldPathsByCollection","orderableFieldName","orderField","name","type","admin","disableBulkEdit","disabled","disableGroupBy","disableListColumn","disableListFilter","hidden","readOnly","hooks","beforeDuplicate","siblingData","index","collectionConfig","existingFieldNames","Set","field","isTopLevelField","joinPath","parentIndexPath","parentIsLocalized","parentSchemaPath","requireFieldLevelRichTextEditor","validRelationships","fields","unshift","beforeChange","orderBeforeChangeHook","data","originalDoc","req","joinScopeWhere","collectionSlug","slug","lastDoc","payload","find","depth","limit","pagination","select","sort","where","exists","undefined","lastOrderValue","docs","push","addOrderableEndpoint","reorderHandler","body","json","docsToMove","newKeyWillBe","target","Array","isArray","length","Response","JSON","stringify","error","headers","collections","c","targetDoc","access","update","id","key","doc","e","Error","message","INTERNAL_SERVER_ERROR","success","targetId","targetKey","targetID","adjacentDoc","adjacentDocKey","orderValues","entries","reorderEndpoint","handler","method","path","endpoints"],"mappings":"AAAA,SAASA,UAAUC,UAAU,QAAQ,cAAa;AAOlD,SAASC,aAAa,QAAQ,8BAA6B;AAC3D,SAASC,QAAQ,QAAQ,wBAAuB;AAChD,SAASC,aAAa,QAAQ,kCAAiC;AAC/D,SAASC,uBAAuB,QAAQ,6CAA4C;AACpF,SAASC,iBAAiB,QAAQ,uCAAsC;AACxE,SAASC,eAAe,QAAQ,qCAAoC;AACpE,SAASC,eAAe,QAAQ,qCAAoC;AACpE,SAASC,kBAAkB,EAAEC,oBAAoB,QAAQ,2BAA0B;AACnF,SAASC,mBAAmB,QAAQ,iCAAgC;AACpE,SAASC,4BAA4B,QAAQ,0CAAyC;AACtF,SAASC,uBAAuB,QAAQ,qCAAoC;AAE5E,OAAO,MAAMC,4BAA4B,OACvCC,YACAC,QACAC,qBACAC;IAEA,gBAAgB;IAChB,KAAK,MAAMC,sBAAsBF,oBAAqB;QACpD,MAAMG,aAAwB;YAC5BC,MAAMF;YACNG,MAAM;YACNC,OAAO;gBACLC,iBAAiB;gBACjBC,UAAU;gBACVC,gBAAgB;gBAChBC,mBAAmB;gBACnBC,mBAAmB;gBACnBC,QAAQ;gBACRC,UAAU;YACZ;YACAC,OAAO;gBACLC,iBAAiB;oBACf,CAAC,EAAEC,WAAW,EAAE;wBACd,OAAOA,WAAW,CAACd,mBAAmB;oBACxC;iBACD;YACH;YACAe,OAAO;QACT;QAEA,2DAA2D;QAC3D,MAAM9B,cAAc;YAClB+B,kBAAkBpB;YAClBC;YACAoB,oBAAoB,IAAIC;YACxBC,OAAOlB;YACPc,OAAO;YACPK,iBAAiB;YACjBC,UAAU;YACVC,iBAAiB;YACjBC,mBAAmB;YACnBC,kBAAkB;YAClBC,iCAAiC;YACjCC,oBAAoB;QACtB;QAEA9B,WAAW+B,MAAM,CAACC,OAAO,CAAC3B;IAC5B;IAEA,cAAc;IACd,IAAI,CAACL,WAAWgB,KAAK,EAAE;QACrBhB,WAAWgB,KAAK,GAAG,CAAC;IACtB;IACA,IAAI,CAAChB,WAAWgB,KAAK,CAACiB,YAAY,EAAE;QAClCjC,WAAWgB,KAAK,CAACiB,YAAY,GAAG,EAAE;IACpC;IAEA,MAAMC,wBAA0C,OAAO,EAAEC,IAAI,EAAEC,WAAW,EAAEC,GAAG,EAAE;QAC/E,KAAK,MAAMjC,sBAAsBF,oBAAqB;YACpD,IAAI,CAACiC,IAAI,CAAC/B,mBAAmB,IAAI,CAACgC,aAAa,CAAChC,mBAAmB,EAAE;gBACnE,MAAMkC,iBAAiBzC,6BAA6B;oBAClD0C,gBAAgBvC,WAAWwC,IAAI;oBAC/BL;oBACAhC;oBACAC;oBACAgC;gBACF;gBAEA,MAAMK,UAAU,MAAMJ,IAAIK,OAAO,CAACC,IAAI,CAAC;oBACrC3C,YAAYA,WAAWwC,IAAI;oBAC3BI,OAAO;oBACPC,OAAO;oBACPC,YAAY;oBACZT;oBACAU,QAAQ;wBAAE,CAAC3C,mBAAmB,EAAE;oBAAK;oBACrC4C,MAAM,CAAC,CAAC,EAAE5C,oBAAoB;oBAC9B6C,OAAO3D,wBAAwB;wBAC7B;4BACE,CAACc,mBAAmB,EAAE;gCACpB8C,QAAQ;4BACV;wBACF;wBACAZ,kBAAkBa;qBACnB;gBACH;gBAEA,MAAMC,iBAAiBX,QAAQY,IAAI,CAAC,EAAE,EAAE,CAACjD,mBAAmB,IAAI;gBAChE+B,IAAI,CAAC/B,mBAAmB,GAAGV,mBAAmB0D,gBAAgB;YAChE;QACF;QAEA,OAAOjB;IACT;IAEAnC,WAAWgB,KAAK,CAACiB,YAAY,CAACqB,IAAI,CAACpB;AACrC,EAAC;AAiBD,OAAO,MAAMqB,uBAAuB,CAClCtD,QACAE;IAEA,kBAAkB;IAClB,MAAMqD,iBAAiC,OAAOnB;QAC5C,MAAMoB,OAAQ,MAAMpB,IAAIqB,IAAI;QAE5B,MAAM,EAAEnB,cAAc,EAAEoB,UAAU,EAAEC,YAAY,EAAExD,kBAAkB,EAAEyD,MAAM,EAAE,GAAGJ;QAEjF,IAAI,CAACK,MAAMC,OAAO,CAACJ,eAAeA,WAAWK,MAAM,KAAK,GAAG;YACzD,OAAO,IAAIC,SAASC,KAAKC,SAAS,CAAC;gBAAEC,OAAO;YAAuC,IAAI;gBACrFC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QACA,IAAI2E,iBAAiB,aAAaA,iBAAiB,QAAQ;YACzD,OAAO,IAAIK,SAASC,KAAKC,SAAS,CAAC;gBAAEC,OAAO;YAA2C,IAAI;gBACzFC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QACA,MAAMe,aAAaC,OAAOqE,WAAW,CAAC3B,IAAI,CAAC,CAAC4B,IAAMA,EAAE/B,IAAI,KAAKD;QAC7D,IAAI,CAACvC,YAAY;YACf,OAAO,IAAIiE,SAASC,KAAKC,SAAS,CAAC;gBAAEC,OAAO,CAAC,WAAW,EAAE7B,eAAe,UAAU,CAAC;YAAC,IAAI;gBACvF8B,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QACA,IAAI,OAAOmB,uBAAuB,UAAU;YAC1C,OAAO,IAAI6D,SAASC,KAAKC,SAAS,CAAC;gBAAEC,OAAO;YAAsC,IAAI;gBACpFC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QAEA,MAAM,EAAEqD,cAAc,EAAEkC,SAAS,EAAE,GAAG,MAAM5E,oBAAoB;YAC9D2C,gBAAgBvC,WAAWwC,IAAI;YAC/BrC;YACAC;YACAiC;YACAwB;QACF;QAEA,8DAA8D;QAC9D,IAAI7D,WAAWyE,MAAM,EAAEC,QAAQ;YAC7B,MAAMvF,cACJ;gBACE,2FAA2F;gBAC3F,mDAAmD;gBACnDwF,IAAIhB,UAAU,CAAC,EAAE;gBACjBxB,MAAM,CAAC;gBACPE;YACF,GACArC,WAAWyE,MAAM,CAACC,MAAM;QAE5B;QACA;;;;;;;;KAQC,GACD,IAAI,CAACb,OAAOe,GAAG,EAAE;YACf,MAAM,EAAEvB,IAAI,EAAE,GAAG,MAAMhB,IAAIK,OAAO,CAACC,IAAI,CAAC;gBACtC3C,YAAYA,WAAWwC,IAAI;gBAC3BI,OAAO;gBACPC,OAAO;gBACPR;gBACAU,QAAQ;oBAAE,CAAC3C,mBAAmB,EAAE;gBAAK;gBACrC6C,OAAO3D,wBAAwB;oBAC7B;wBACE,CAACc,mBAAmB,EAAE;4BACpB8C,QAAQ;wBACV;oBACF;oBACAZ,kBAAkBa;iBACnB;YACH;YACA,MAAM3D,gBAAgB6C;YACtB,8EAA8E;YAC9E,gEAAgE;YAChE,IAAI;gBACF,KAAK,MAAMwC,OAAOxB,KAAM;oBACtB,MAAMhB,IAAIK,OAAO,CAACgC,MAAM,CAAC;wBACvBC,IAAIE,IAAIF,EAAE;wBACV3E,YAAYA,WAAWwC,IAAI;wBAC3BL,MAAM;wBAEN;wBACAS,OAAO;wBACPP;oBACF;oBACA,MAAM9C,kBAAkB8C;gBAC1B;YACF,EAAE,OAAOyC,GAAG;gBACV,MAAMrF,gBAAgB4C;gBACtB,IAAIyC,aAAaC,OAAO;oBACtB,MAAM,IAAI3F,SAAS0F,EAAEE,OAAO,EAAE9F,WAAW+F,qBAAqB;gBAChE;YACF;YAEA,OAAO,IAAIhB,SAASC,KAAKC,SAAS,CAAC;gBAAEa,SAAS;gBAAqBE,SAAS;YAAK,IAAI;gBACnFb,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QAEA,IACE,OAAO4E,WAAW,YAClB,OAAOA,OAAOc,EAAE,KAAK,eACrB,OAAOd,OAAOe,GAAG,KAAK,UACtB;YACA,OAAO,IAAIX,SAASC,KAAKC,SAAS,CAAC;gBAAEC,OAAO;YAAmC,IAAI;gBACjFC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CpF,QAAQ;YACV;QACF;QAEA,MAAMkG,WAAWtB,OAAOc,EAAE;QAC1B,MAAMS,YAAY,MAAMtF,wBAAwB;YAC9CyC,gBAAgBvC,WAAWwC,IAAI;YAC/BpC;YACAiC;YACAmC;YACAa,UAAUF;YACVC,WAAWvB,OAAOe,GAAG;QACvB;QAEA,mFAAmF;QACnF,0FAA0F;QAC1F,uFAAuF;QACvF,MAAMU,cAAc,MAAMjD,IAAIK,OAAO,CAACC,IAAI,CAAC;YACzC3C,YAAYA,WAAWwC,IAAI;YAC3BI,OAAO;YACPC,OAAO;YACPC,YAAY;YACZC,QAAQ;gBAAE,CAAC3C,mBAAmB,EAAE;YAAK;YACrC4C,MAAMY,iBAAiB,YAAYxD,qBAAqB,CAAC,CAAC,EAAEA,oBAAoB;YAChF6C,OAAO3D,wBAAwB;gBAC7B;oBACE,CAACc,mBAAmB,EAAE;wBACpB,CAACwD,iBAAiB,YAAY,iBAAiB,YAAY,EAAEwB;oBAC/D;gBACF;gBACA9C,kBAAkBa;aACnB;QACH;QACA,MAAMoC,iBAAiBD,YAAYjC,IAAI,EAAE,CAAC,EAAE,EAAE,CAACjD,mBAAmB,IAAI;QAEtE,6EAA6E;QAC7E,uEAAuE;QACvE,MAAMoF,cACJ5B,iBAAiB,YACbjE,qBAAqByF,WAAWG,gBAAgB5B,WAAWK,MAAM,IACjErE,qBAAqB4F,gBAAgBH,WAAWzB,WAAWK,MAAM;QAEvE,gDAAgD;QAChD,KAAK,MAAM,CAAC7C,OAAOwD,GAAG,IAAIhB,WAAW8B,OAAO,GAAI;YAC9C,MAAMpD,IAAIK,OAAO,CAACgC,MAAM,CAAC;gBACvBC;gBACA3E,YAAYA,WAAWwC,IAAI;gBAC3BL,MAAM;oBACJ,CAAC/B,mBAAmB,EAAEoF,WAAW,CAACrE,MAAM;gBAC1C;gBACAyB,OAAO;gBACPP;YACF;QACF;QAEA,OAAO,IAAI4B,SAASC,KAAKC,SAAS,CAAC;YAAEqB;YAAaN,SAAS;QAAK,IAAI;YAClEb,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CpF,QAAQ;QACV;IACF;IAEA,MAAMyG,kBAA4B;QAChCC,SAASnC;QACToC,QAAQ;QACRC,MAAM;IACR;IAEA,IAAI,CAAC5F,OAAO6F,SAAS,EAAE;QACrB7F,OAAO6F,SAAS,GAAG,EAAE;IACvB;IAEA7F,OAAO6F,SAAS,CAACxC,IAAI,CAACoC;AACxB,EAAC"}