     1	import {
     2	  calculatePosition,
     3	  getInitialPosition,
     4	  isValidPosition,
     5	  assignInitialPositions,
     6	} from './lib/fractional-positioning.js'
     7	import { createStorageAdapter } from './lib/storage/storage-adapter.js'
     8	
     9	export const PositioningPlugin = {
    10	  name: 'positioning',
    11	  dependencies: ['rest-api', 'rest-api-knex|rest-api-anyapi-knex'],
    12	
    13	  install ({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
    14	    const installedPlugins = Array.from(api._installedPlugins || [])
    15	    const legacyStorageInstalled = installedPlugins.includes('rest-api-knex')
    16	    const canonicalStorageInstalled = installedPlugins.includes('rest-api-anyapi-knex')
    17	    if (!legacyStorageInstalled && !canonicalStorageInstalled) {
    18	      throw new Error(
    19	        "Positioning plugin requires either 'rest-api-knex' or 'rest-api-anyapi-knex' to be installed before it."
    20	      )
    21	    }
    22	
    23	    if (!api.knex?.instance) {
    24	      throw new Error('Positioning plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
    25	    }
    26	
    27	    // Get configuration - hooked-api namespaces options by plugin name
    28	    const positioningOptions = pluginOptions || {}
    29	
    30	    // Store configuration in vars (data only) - inspired by multihome pattern
    31	    vars.positioning = {
    32	      field: positioningOptions.field || 'position',
    33	      filters: positioningOptions.filters || [],
    34	      excludeResources: positioningOptions.excludeResources || ['system_migrations', 'system_logs'],
    35	      strategy: positioningOptions.strategy || 'fractional',
    36	      beforeIdField: positioningOptions.beforeIdField || 'beforeId',
    37	      defaultPosition: positioningOptions.defaultPosition || 'last',
    38	      autoIndex: positioningOptions.autoIndex !== undefined ? positioningOptions.autoIndex : true,
    39	      rebalanceThreshold: positioningOptions.rebalanceThreshold || 50 // Max position string length
    40	    }
    41	
    42	    const resolveStorageAdapter = (scopeName) => {
    43	      if (!scopeName) return null
    44	      const scope = scopes[scopeName] || api.resources?.[scopeName]
    45	      const scopeVars = scope?.vars
    46	      if (!scopeVars) return null
    47	
    48	      if (scopeVars.storageAdapter) {
    49	        return scopeVars.storageAdapter
    50	      }
    51	
    52	      if (scopeVars.schemaInfo && api.knex?.instance) {
    53	        const adapter = createStorageAdapter({ knex: api.knex.instance, schemaInfo: scopeVars.schemaInfo })
    54	        scopeVars.storageAdapter = adapter
    55	        return adapter
    56	      }
    57	
    58	      return null
    59	    }
    60	
    61	    // Validate configuration
    62	    if (!['fractional', 'integer'].includes(vars.positioning.strategy)) {
    63	      throw new Error(`Invalid positioning strategy: ${vars.positioning.strategy}. Must be 'fractional' or 'integer'`)
    64	    }
    65	
    66	    if (vars.positioning.strategy === 'integer') {
    67	      throw new Error("Integer positioning strategy not yet implemented. Please use 'fractional'")
    68	    }
    69	
    70	    // Helper to check if a resource should have positioning
    71	    function shouldHavePositioning (scopeName) {
    72	      return !vars.positioning.excludeResources.includes(scopeName)
    73	    }
    74	
    75	    // Helper to build filter conditions for position groups
    76	    function buildPositionFilterConditions (adapter, schemaInfo, {
    77	      attributes = {},
    78	      rawRecord = {},
    79	      simplifiedRecord = {},
    80	      inputRelationships = {},
    81	      minimalRelationships = {},
    82	    }) {
    83	      const conditions = {}
    84	
    85	      log.debug('*** buildPositionFilterConditions', {
    86	        filters: vars.positioning.filters,
    87	        attributeKeys: Object.keys(attributes || {}),
    88	        rawKeys: Object.keys(rawRecord || {}),
    89	        simplifiedKeys: Object.keys(simplifiedRecord || {}),
    90	      })
    91	
    92	      const tryAssign = (holder, field) => (holder && field in holder ? holder[field] : undefined)
    93	
    94	      vars.positioning.filters.forEach((filterField) => {
    95	        const searchField = schemaInfo.searchSchemaStructure?.[filterField] || null
    96	        const schemaField = schemaInfo.schemaStructure?.[filterField] || null
    97	        const isRelationship = Boolean(
    98	          searchField?.isRelationship ||
    99	          schemaField?.belongsTo ||
   100	          schemaField?.belongsToPolymorphic
   101	        )
   102	
   103	        if (!searchField && !schemaField) {
   104	          log.warn(`Filter field '${filterField}' not found in search schema or resource schema for positioning`)
   105	        }
   106	
   107	        const dbField = searchField?.actualField || filterField
   108	        let value
   109	
   110	        const attempt = (candidate) => {
   111	          if (value === undefined && candidate !== undefined) {
   112	            value = candidate
   113	          }
   114	        }
   115	
   116	        attempt(tryAssign(rawRecord, filterField))
   117	        attempt(tryAssign(simplifiedRecord, filterField))
   118	        attempt(tryAssign(rawRecord, dbField))
   119	        attempt(tryAssign(attributes, filterField))
   120	        attempt(tryAssign(attributes, dbField))
   121	
   122	        if (isRelationship) {
   123	          attempt(inputRelationships?.[filterField]?.data?.id)
   124	          attempt(minimalRelationships?.[filterField]?.data?.id)
   125	          // Some contexts may expose relationship data under attributes
   126	          attempt(tryAssign(rawRecord?.relationships, filterField)?.data?.id)
   127	        }
   128	
   129	        if (value !== undefined) {
   130	          const column = adapter.translateColumn(dbField)
   131	          const normalizedValue = value === null
   132	            ? null
   133	            : adapter.translateFilterValue(filterField, value)
   134	          conditions[column] = normalizedValue
   135	        }
   136	      })
   137	
   138	      log.debug('*** Filter conditions built', { conditions })
   139	      return conditions
   140	    }
   141	
   142	    // Validate that resources have position field when added
   143	    addHook('scope:added', 'validate-position-field', {}, ({ context, vars }) => {
   144	      const { scopeName, scopeOptions } = context
   145	
   146	      // Skip excluded resources
   147	      if (!shouldHavePositioning(scopeName)) {
   148	        log.debug(`Resource ${scopeName} excluded from positioning`)
   149	        return
   150	      }
   151	
   152	      // Check if schema has position field
   153	      const schema = scopeOptions.schema
   154	      if (schema && !schema[vars.positioning.field]) {
   155	        throw new Error(
   156	          `Resource '${scopeName}' must have '${vars.positioning.field}' field in schema to use positioning plugin`
   157	        )
   158	      }
   159	
   160	      // Validate filter fields exist (check both relationship names and actual field names)
   161	      vars.positioning.filters.forEach(filterField => {
   162	        // Check if it's a relationship field by looking for a field with matching 'as' property
   163	        let fieldExists = false
   164	
   165	        // First check if the field exists directly in the schema
   166	        if (schema[filterField]) {
   167	          fieldExists = true
   168	        } else {
   169	          // Check if it's a relationship name (as property)
   170	          for (const [fieldName, fieldDef] of Object.entries(schema)) {
   171	            if (fieldDef.as === filterField) {
   172	              fieldExists = true
   173	              break
   174	            }
   175	          }
   176	        }
   177	
   178	        if (!fieldExists) {
   179	          throw new Error(
   180	            `Resource '${scopeName}' must have '${filterField}' field or relationship in schema for position filtering`
   181	          )
   182	        }
   183	      })
   184	    })
   185	
   186	    // Add index for position field if autoIndex is enabled
   187	    addHook('scope:added', 'add-position-index', { afterFunction: 'validate-position-field' }, async ({ context, vars, scopes }) => {
   188	      const { scopeName } = context
   189	
   190	      if (!shouldHavePositioning(scopeName) || !vars.positioning.autoIndex) {
   191	        return
   192	      }
   193	
   194	      const scope = scopes[scopeName]
   195	      const schemaInfo = scope.vars.schemaInfo
   196	
   197	      if (!schemaInfo || !api.knex?.instance) {
   198	        return // Skip if no database connection
   199	      }
   200	
   201	      try {
   202	        const adapter = resolveStorageAdapter(scopeName)
   203	        if (adapter.isCanonical()) {
   204	          log.debug(`Skipping positioning index for canonical storage on ${scopeName}`)
   205	          return
   206	        }
   207	
   208	        const knex = api.knex.instance
   209	        const tableName = adapter.getTableName()
   210	
   211	        // Check if table exists first
   212	        const tableExists = await knex.schema.hasTable(tableName)
   213	        if (!tableExists) {
   214	          log.debug(`Table ${tableName} doesn't exist yet, skipping index creation`)
   215	          return
   216	        }
   217	
   218	        // Build index columns: map filter fields to actual database columns
   219	        const indexColumns = new Set()
   220	
   221	        // Map each filter field to its database column
   222	        vars.positioning.filters.forEach(filterField => {
   223	          const searchField = schemaInfo.searchSchemaStructure?.[filterField]
   224	          if (searchField) {
   225	            // Use actualField if it exists (for relationships)
   226	            const dbField = searchField.actualField || filterField
   227	            indexColumns.add(adapter.translateColumn(dbField))
   228	          } else if (schemaInfo.schemaStructure[filterField]) {
   229	            // Direct database field
   230	            indexColumns.add(adapter.translateColumn(filterField))
   231	          }
   232	        })
   233	
   234	        // Add the position field
   235	        indexColumns.add(adapter.translateColumn(vars.positioning.field))
   236	
   237	        const indexColumnList = [...indexColumns].filter(Boolean)
   238	
   239	        const indexName = `idx_${tableName}_positioning`
   240	
   241	        // Check if index already exists
   242	        const hasIndex = await knex.schema.hasIndex(tableName, indexColumnList, indexName)
   243	
   244	        if (!hasIndex) {
   245	          await knex.schema.table(tableName, table => {
   246	            table.index(indexColumnList, indexName)
   247	          })
   248	
   249	          log.info(`Created positioning index on ${tableName}`, { columns: indexColumnList })
   250	        }
   251	      } catch (error) {
   252	        log.warn(`Could not create positioning index for ${scopeName}:`, error.message)
   253	      }
   254	    })
   255	
   256	    // Process beforeId parameter in requests
   257	    addHook('beforeSchemaValidate', 'process-beforeid', {}, async ({ context, scopeName, vars }) => {
   258	      log.debug('*** beforeSchemaValidate positioning hook', {
   259	        scopeName,
   260	        method: context.method,
   261	        shouldHavePositioning: shouldHavePositioning(scopeName)
   262	      })
   263	
   264	      // Skip excluded resources
   265	      if (!shouldHavePositioning(scopeName)) {
   266	        log.debug('*** Skipping beforeId processing - excluded resource')
   267	        return
   268	      }
   269	
   270	      // Skip if no positioning context needed
   271	      const method = context.method
   272	      if (!['post', 'put', 'patch'].includes(method)) {
   273	        log.debug('*** Skipping beforeId processing - wrong method', { method })
   274	        return
   275	      }
   276	
   277	      // Check if resource has position field
   278	      const scope = scopes[scopeName]
   279	      const hasPositionField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.positioning.field]
   280	
   281	      if (!hasPositionField) {
   282	        return
   283	      }
   284	
   285	      // Extract beforeId from JSON:API format attributes
   286	      const attributes = context.inputRecord?.data?.attributes ||
   287	        (context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
   288	          ? context.inputRecord
   289	          : {})
   290	      const beforeId = attributes[vars.positioning.beforeIdField]
   291	
   292	      // Store beforeId in context for later use
   293	      if (beforeId !== undefined) {
   294	        context.positioningBeforeId = beforeId
   295	
   296	        // Remove beforeId from attributes so it doesn't get stored
   297	        delete attributes[vars.positioning.beforeIdField]
   298	
   299	        if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
   300	          delete context.inputRecord[vars.positioning.beforeIdField]
   301	        }
   302	      }
   303	
   304	      // Remove any manually provided position field - position is managed by this plugin
   305	      if (attributes[vars.positioning.field] !== undefined) {
   306	        delete attributes[vars.positioning.field]
   307	
   308	        log.debug('Positioning beforeId extracted', {
   309	          scopeName,
   310	          beforeId,
   311	          method
   312	        })
   313	
   314	        if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
   315	          delete context.inputRecord[vars.positioning.field]
   316	        }
   317	      }
   318	
   319	      // For new records without explicit position, mark for positioning
   320	      if (method === 'post' && !attributes[vars.positioning.field]) {
   321	        context.needsPositioning = true
   322	      }
   323	    })
   324	
   325	    // Calculate and set position before create/update
   326	    addHook('beforeDataCallPost', 'calculate-position-post', {}, async ({ context, scopeName, vars, helpers }) => {
   327	      log.debug('*** beforeDataCallPost hook fired for positioning', { scopeName, method: context.method })
   328	      await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
   329	    })
   330	
   331	    addHook('beforeDataCallPut', 'calculate-position-put', {}, async ({ context, scopeName, vars, helpers }) => {
   332	      log.debug('*** beforeDataCallPut hook fired for positioning', { scopeName, method: context.method })
   333	      // Only process if beforeId was provided or it's a new record
   334	      if (context.positioningBeforeId !== undefined || context.needsPositioning) {
   335	        await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
   336	      }
   337	    })
   338	
   339	    addHook('beforeDataCallPatch', 'calculate-position-patch', {}, async ({ context, scopeName, vars, helpers }) => {
   340	      log.debug('*** beforeDataCallPatch hook fired for positioning', { scopeName, method: context.method })
   341	      // Only process if beforeId was provided
   342	      if (context.positioningBeforeId !== undefined) {
   343	        await calculateAndSetPosition(context, scopeName, vars, helpers, log, scopes, api)
   344	      }
   345	    })
   346	
   347	    // Helper function to calculate and set position
   348	    async function calculateAndSetPosition (context, scopeName, vars, helpers, log, scopes, api) {
   349	      log.debug('*** calculateAndSetPosition called', {
   350	        scopeName,
   351	        method: context.method,
   352	        shouldHavePositioning: shouldHavePositioning(scopeName),
   353	        beforeId: context.positioningBeforeId,
   354	        needsPositioning: context.needsPositioning
   355	      })
   356	
   357	      // Skip if no positioning needed
   358	      if (!shouldHavePositioning(scopeName)) {
   359	        log.debug('*** Skipping - resource excluded from positioning')
   360	        return
   361	      }
   362	
   363	      const beforeId = context.positioningBeforeId
   364	      const needsPositioning = context.needsPositioning
   365	
   366	      // Skip if no positioning action needed
   367	      if (beforeId === undefined && !needsPositioning) {
   368	        log.debug('*** Skipping - no positioning needed', { beforeId, needsPositioning })
   369	        return
   370	      }
   371	
   372	      const recordData = (() => {
   373	        if (context.inputRecord?.data?.attributes) {
   374	          return { ...context.inputRecord.data.attributes }
   375	        }
   376	        if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
   377	          return { ...context.inputRecord }
   378	        }
   379	        return {}
   380	      })()
   381	
   382	      log.debug('*** Record data for positioning', {
   383	        recordData,
   384	        simplified: context.simplified,
   385	        inputRecord: context.inputRecord
   386	      })
   387	
   388	      const jsonApiRecord = context.inputRecord?.data || {}
   389	      const inputRelationships = jsonApiRecord.relationships || {}
   390	      const simplifiedRecord = context.simplified && context.inputRecord && typeof context.inputRecord === 'object'
   391	        ? context.inputRecord
   392	        : {}
   393	
   394	      const minimalAttributes = context.minimalRecord?.attributes || {}
   395	      const minimalRelationships = context.minimalRecord?.relationships || {}
   396	
   397	      const combinedAttributes = {
   398	        ...minimalAttributes,
   399	        ...recordData,
   400	      }
   401	
   402	      // Build filter conditions for the position group
   403	      const schemaInfo = context.schemaInfo
   404	      const adapter = context.storageAdapter || resolveStorageAdapter(scopeName)
   405	
   406	      if (!adapter) {
   407	        log.warn('*** Positioning skipped - no storage adapter available', { scopeName })
   408	        return
   409	      }
   410	
   411	      const filterConditions = buildPositionFilterConditions(adapter, schemaInfo, {
   412	        attributes: combinedAttributes,
   413	        rawRecord: recordData,
   414	        simplifiedRecord,
   415	        inputRelationships,
   416	        minimalRelationships,
   417	      })
   418	
   419	      log.debug('*** Position filter conditions', { scopeName, filterConditions })
   420	
   421	      // Query existing items in the same position group
   422	      const knex = api.knex.instance
   423	      const tableName = adapter.getTableName()
   424	      const idProperty = schemaInfo.idProperty
   425	      const idColumn = adapter.getIdColumn()
   426	      const positionColumn = adapter.translateColumn(vars.positioning.field)
   427	
   428	      log.debug('*** Building position query', {
   429	        tableName,
   430	        idProperty,
   431	        idColumn,
   432	        positionField: vars.positioning.field,
   433	        positionColumn,
   434	        filterConditions
   435	      })
   436	
   437	      // Check if we have a transaction to use
   438	      // Build base query with filter conditions
   439	      const baseQuery = adapter.buildBaseQuery({ transaction: context.transaction })
   440	      Object.entries(filterConditions).forEach(([column, value]) => {
   441	        if (value === null) {
   442	          baseQuery.whereNull(column)
   443	        } else {
   444	          baseQuery.where(column, value)
   445	        }
   446	      })
   447	
   448	      // For updates, exclude the current record
   449	      if ((context.method === 'patch' || context.method === 'put') && context.id) {
   450	        const translatedId = adapter.translateFilterValue(idProperty, context.id)
   451	        baseQuery.whereNot(idColumn, translatedId)
   452	      }
   453	
   454	      // Calculate effective beforeId first
   455	      let effectiveBeforeId = beforeId
   456	
   457	      // Handle default positioning when no beforeId is provided
   458	      if (beforeId === undefined) {
   459	        if (vars.positioning.defaultPosition === 'last') {
   460	          effectiveBeforeId = null // null means position at end
   461	        } else if (vars.positioning.defaultPosition === 'first') {
   462	          effectiveBeforeId = 'FIRST' // Special marker for first position
   463	        }
   464	      }
   465	
   466	      let items = []
   467	
   468	      const selectColumns = {
   469	        [idProperty]: idColumn,
   470	        [vars.positioning.field]: positionColumn,
   471	      }
   472	      const selectWithAliases = (builder) => adapter.selectColumns(builder, selectColumns)
   473	
   474	      try {
   475	        log.debug('*** Positioning query start', {
   476	          effectiveBeforeId,
   477	          selectColumns,
   478	        })
   479	        if (effectiveBeforeId === null) {
   480	          // Positioning at end - only get the last item
   481	          const lastItem = await selectWithAliases(baseQuery
   482	            .clone())
   483	            .orderBy(positionColumn, 'desc')
   484	            .first()
   485	
   486	          if (lastItem) {
   487	            items = [lastItem]
   488	          }
   489	        } else if (effectiveBeforeId === 'FIRST') {
   490	          // Positioning at beginning - get the first item
   491	          const firstItem = await selectWithAliases(baseQuery
   492	            .clone())
   493	            .orderBy(positionColumn, 'asc')
   494	            .first()
   495	
   496	          if (firstItem) {
   497	            items = [firstItem]
   498	            effectiveBeforeId = firstItem[idProperty]
   499	          } else {
   500	            effectiveBeforeId = null
   501	          }
   502	        } else if (effectiveBeforeId) {
   503	          // Positioning before a specific item - get that item and the one before it
   504	          const normalizedTargetId = adapter.translateFilterValue(idProperty, effectiveBeforeId)
   505	          const targetItem = await selectWithAliases(baseQuery
   506	            .clone())
   507	            .where(idColumn, normalizedTargetId)
   508	            .first()
   509	
   510	          if (targetItem) {
   511	            // Get the item immediately before the target
   512	            const prevItem = await selectWithAliases(baseQuery
   513	              .clone())
   514	              .where(positionColumn, '<', targetItem[vars.positioning.field])
   515	              .orderBy(positionColumn, 'desc')
   516	              .first()
   517	
   518	            items = prevItem ? [prevItem, targetItem] : [targetItem]
   519	          }
   520	        } else {
   521	          // First item in group - no items needed
   522	          items = []
   523	        }
   524	
   525	        log.debug('*** Query completed', { itemCount: items.length, items })
   526	      } catch (error) {
   527	        log.error('*** Query failed', { error: error.message, stack: error.stack })
   528	        throw error
   529	      }
   530	
   531	      // Calculate new position
   532	      const newPosition = items.length === 0
   533	        ? getInitialPosition()
   534	        : calculatePosition(items, effectiveBeforeId, idProperty, vars.positioning.field)
   535	
   536	      // Set the position on the incoming payload so downstream steps persist it
   537	      if (context.inputRecord?.data) {
   538	        context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
   539	        context.inputRecord.data.attributes[vars.positioning.field] = newPosition
   540	      }
   541	
   542	      if (context.simplified && context.inputRecord && typeof context.inputRecord === 'object') {
   543	        context.inputRecord[vars.positioning.field] = newPosition
   544	      }
   545	
   546	      log.debug('Position calculated', {
   547	        scopeName,
   548	        newPosition,
   549	        beforeId: effectiveBeforeId,
   550	        filterConditions,
   551	        itemCount: items.length,
   552	        method: context.method,
   553	        id: context.id
   554	      })
   555	    }
   556	
   557	    // Add positioning info back to response
   558	    addHook('afterGet', 'add-beforeid-to-response', {}, async ({ context, vars }) => {
   559	      // Only add if beforeId was in the original request
   560	      if (context.positioningBeforeId !== undefined && context.record) {
   561	        if (context.simplified) {
   562	          context.record[vars.positioning.beforeIdField] = context.positioningBeforeId
   563	        } else {
   564	          // For JSON:API, add to meta
   565	          context.meta = context.meta || {}
   566	          context.meta.positioning = {
   567	            [vars.positioning.beforeIdField]: context.positioningBeforeId
   568	          }
   569	        }
   570	      }
   571	    })
   572	
   573	    // Apply default sort by position if no other sort specified
   574	    addHook('beforeQuery', 'apply-position-sort', {}, async ({ context, scopeName, vars }) => {
   575	      if (!shouldHavePositioning(scopeName)) {
   576	        return
   577	      }
   578	
   579	      // Check if sort is already specified
   580	      const hasSort = context.queryParams?.sort && context.queryParams.sort.length > 0
   581	
   582	      if (!hasSort) {
   583	        // Apply position sort
   584	        context.queryParams = context.queryParams || {}
   585	        context.queryParams.sort = [vars.positioning.field]
   586	
   587	        log.trace('Applied default position sort', { scopeName })
   588	      }
   589	    })
   590	
   591	    // API methods for positioning operations
   592	    api.positioning = {
   593	      /**
   594	       * Reorder items by providing new positions
   595	       * @param {string} scopeName - Resource scope name
   596	       * @param {Array} positions - Array of {id, position} or {id, beforeId}
   597	       * @param {Object} filters - Filter conditions for position group
   598	       */
   599	      async reorder (scopeName, positions, filters = {}) {
   600	        const scope = scopes[scopeName]
   601	        if (!scope) {
   602	          throw new Error(`Unknown resource: ${scopeName}`)
   603	        }
   604	
   605	        // Implementation would go here for bulk reordering
   606	        log.info('Bulk reorder requested', { scopeName, count: positions.length })
   607	      },
   608	
   609	      /**
   610	       * Get positioning configuration
   611	       */
   612	      getConfig () {
   613	        return { ...vars.positioning }
   614	      },
   615	
   616	      /**
   617	       * Check if a resource has positioning enabled
   618	       */
   619	      isEnabled (scopeName) {
   620	        return shouldHavePositioning(scopeName)
   621	      }
   622	    }
   623	
   624	    log.info('Positioning plugin installed', {
   625	      field: vars.positioning.field,
   626	      strategy: vars.positioning.strategy,
   627	      filters: vars.positioning.filters,
   628	      excludedResources: vars.positioning.excludeResources
   629	    })
   630	  }
   631	}
   632	
   633	/*
   634	Usage examples:
   635	
   636	// Basic usage - positioning for all resources
   637	await api.use(PositioningPlugin);
   638	
   639	// With configuration
   640	await api.use(PositioningPlugin, {
   641	  field: 'sortOrder',
   642	  filters: ['status', 'projectId'],
   643	  excludeResources: ['users', 'logs']
   644	});
   645	
   646	// Position grouping example
   647	await api.use(PositioningPlugin, {
   648	  filters: ['boardId', 'listId'], // Separate positions per board/list combo
   649	  defaultPosition: 'last'
   650	});
   651	
   652	// In requests:
   653	// POST /api/tasks
   654	{
   655	  "title": "New Task",
   656	  "boardId": 123,
   657	  "listId": 456,
   658	  "beforeId": "task-789"  // Position before this task
   659	}
   660	
   661	// Or to add at end:
   662	{
   663	  "title": "New Task",
   664	  "boardId": 123,
   665	  "listId": 456,
   666	  "beforeId": null  // Explicit last position
   667	}
   668	*/
