json-rest-api 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,232 +1,232 @@
1
- import { ensureAnyApiSchema } from './lib/anyapi/schema-utils.js';
2
- import { AnyapiRegistry } from './lib/anyapi/anyapi-registry.js';
3
- import { RestApiValidationError } from '../../lib/rest-api-errors.js';
4
- import { createSchema } from 'json-rest-schema';
1
+ import { ensureAnyApiSchema } from './lib/anyapi/schema-utils.js'
2
+ import { AnyapiRegistry } from './lib/anyapi/anyapi-registry.js'
3
+ import { RestApiValidationError } from '../../lib/rest-api-errors.js'
4
+ import { createSchema } from 'json-rest-schema'
5
5
  import {
6
6
  DEFAULT_QUERY_LIMIT,
7
7
  DEFAULT_MAX_QUERY_LIMIT,
8
- } from './lib/querying-writing/knex-constants.js';
8
+ } from './lib/querying-writing/knex-constants.js'
9
9
  import {
10
10
  calculatePaginationMeta,
11
11
  generatePaginationLinks,
12
12
  generateCursorPaginationLinks,
13
13
  buildCursorMeta,
14
14
  parseCursor,
15
- } from './lib/querying/knex-pagination-helpers.js';
16
- import { getUrlPrefix, buildResourceUrl } from './lib/querying/url-helpers.js';
15
+ } from './lib/querying/knex-pagination-helpers.js'
16
+ import { getUrlPrefix, buildResourceUrl } from './lib/querying/url-helpers.js'
17
17
  import {
18
18
  normalizeId,
19
19
  resolveFieldInfo,
20
20
  coerceValueForDefinition,
21
21
  normalizeFilterValues,
22
22
  ensureFilterableField,
23
- } from './lib/anyapi/utils/descriptor-helpers.js';
23
+ } from './lib/anyapi/utils/descriptor-helpers.js'
24
24
  import {
25
25
  AnyapiQueryAdapter,
26
26
  preloadRelatedDescriptors,
27
- } from './lib/anyapi/query/anyapi-query-adapter.js';
27
+ } from './lib/anyapi/query/anyapi-query-adapter.js'
28
28
  import {
29
29
  ensureSearchFieldsAreIndexed,
30
30
  generateSearchSchemaFromSchema,
31
- } from './lib/querying-writing/schema-helpers.js';
32
- import { buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js';
31
+ } from './lib/querying-writing/schema-helpers.js'
32
+ import { buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
33
33
  import {
34
34
  polymorphicFiltersHook,
35
35
  crossTableFiltersHook,
36
36
  basicFiltersHook,
37
- } from './lib/querying/knex-query-helpers.js';
38
- import { createStorageAdapter } from './lib/storage/storage-adapter.js';
37
+ } from './lib/querying/knex-query-helpers.js'
38
+ import { createStorageAdapter } from './lib/storage/storage-adapter.js'
39
39
 
40
- const DEFAULT_TENANT = 'default';
41
- const LINKS_TABLE = 'any_links';
40
+ const DEFAULT_TENANT = 'default'
41
+ const LINKS_TABLE = 'any_links'
42
42
 
43
43
  export const RestApiAnyapiKnexPlugin = {
44
44
  name: 'rest-api-anyapi-knex',
45
45
  dependencies: ['rest-api'],
46
46
 
47
- async install({ helpers, vars, pluginOptions, api, log, addHook, addScopeMethod, scopes }) {
48
- const options = pluginOptions || {};
49
- const knex = options.knex;
50
- const tenantId = options.tenantId || DEFAULT_TENANT;
47
+ async install ({ helpers, vars, pluginOptions, api, log, addHook, addScopeMethod, scopes }) {
48
+ const options = pluginOptions || {}
49
+ const knex = options.knex
50
+ const tenantId = options.tenantId || DEFAULT_TENANT
51
51
 
52
52
  if (!knex) {
53
- throw new Error('AnyapiKnexPlugin requires a knex instance');
53
+ throw new Error('AnyapiKnexPlugin requires a knex instance')
54
54
  }
55
55
 
56
- await ensureAnyApiSchema(knex);
56
+ await ensureAnyApiSchema(knex)
57
57
 
58
- const registry = new AnyapiRegistry({ knex, log });
58
+ const registry = new AnyapiRegistry({ knex, log })
59
59
 
60
- api.anyapi = api.anyapi || {};
61
- api.anyapi.registry = registry;
62
- api.anyapi.tenantId = tenantId;
60
+ api.anyapi = api.anyapi || {}
61
+ api.anyapi.registry = registry
62
+ api.anyapi.tenantId = tenantId
63
63
  api.knex = {
64
64
  instance: knex,
65
65
  helpers: {},
66
- };
66
+ }
67
67
 
68
- const storageAdapterCache = new Map();
68
+ const storageAdapterCache = new Map()
69
69
 
70
70
  const getScopeStorageAdapter = (scopeName) => {
71
- if (!scopeName) return null;
72
- const resource = api.resources?.[scopeName] || scopes?.[scopeName];
73
- const schemaInfo = resource?.vars?.schemaInfo;
74
- if (!schemaInfo) return null;
71
+ if (!scopeName) return null
72
+ const resource = api.resources?.[scopeName] || scopes?.[scopeName]
73
+ const schemaInfo = resource?.vars?.schemaInfo
74
+ if (!schemaInfo) return null
75
75
 
76
- const cached = storageAdapterCache.get(scopeName);
76
+ const cached = storageAdapterCache.get(scopeName)
77
77
  if (cached && cached.schemaInfo === schemaInfo) {
78
- return cached.adapter;
78
+ return cached.adapter
79
79
  }
80
80
 
81
- const adapter = createStorageAdapter({ knex, schemaInfo });
82
- storageAdapterCache.set(scopeName, { adapter, schemaInfo });
81
+ const adapter = createStorageAdapter({ knex, schemaInfo })
82
+ storageAdapterCache.set(scopeName, { adapter, schemaInfo })
83
83
  if (resource?.vars) {
84
- resource.vars.storageAdapter = adapter;
84
+ resource.vars.storageAdapter = adapter
85
85
  }
86
- return adapter;
87
- };
86
+ return adapter
87
+ }
88
88
 
89
- api.knex.helpers.getStorageAdapter = getScopeStorageAdapter;
90
- helpers.getStorageAdapter = getScopeStorageAdapter;
89
+ api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
90
+ helpers.getStorageAdapter = getScopeStorageAdapter
91
91
 
92
- const scopeOptionsRegistry = new Map();
92
+ const scopeOptionsRegistry = new Map()
93
93
 
94
- helpers.newTransaction = async () => knex.transaction();
94
+ helpers.newTransaction = async () => knex.transaction()
95
95
 
96
96
  addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
97
97
  if (api.knex?.instance) {
98
- await api.knex.instance.destroy();
98
+ await api.knex.instance.destroy()
99
99
  }
100
- });
100
+ })
101
101
 
102
102
  const getDescriptor = async (scopeName) => {
103
- const descriptor = await registry.getDescriptor(tenantId, scopeName);
103
+ const descriptor = await registry.getDescriptor(tenantId, scopeName)
104
104
  if (!descriptor) {
105
- throw new Error(`Descriptor not found for resource '${scopeName}'`);
105
+ throw new Error(`Descriptor not found for resource '${scopeName}'`)
106
106
  }
107
- return descriptor;
108
- };
107
+ return descriptor
108
+ }
109
109
 
110
110
  const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
111
- if (!filters || Object.keys(filters).length === 0) return;
111
+ if (!filters || Object.keys(filters).length === 0) return
112
112
 
113
113
  for (const [field, rawValue] of Object.entries(filters)) {
114
- const searchDef = searchSchema ? searchSchema[field] : null;
114
+ const searchDef = searchSchema ? searchSchema[field] : null
115
115
  if (searchDef) {
116
116
  const isAdvanced = Boolean(
117
- searchDef.actualField?.includes('.')
118
- || (Array.isArray(searchDef.oneOf) && searchDef.oneOf.some((entry) => entry.includes('.')))
119
- || searchDef.polymorphicField
120
- || typeof searchDef.applyFilter === 'function'
121
- );
117
+ searchDef.actualField?.includes('.') ||
118
+ (Array.isArray(searchDef.oneOf) && searchDef.oneOf.some((entry) => entry.includes('.'))) ||
119
+ searchDef.polymorphicField ||
120
+ typeof searchDef.applyFilter === 'function'
121
+ )
122
122
  if (!isAdvanced && adapter) {
123
- const columnRef = adapter.translateColumn(searchDef.actualField || field);
124
- const operator = (searchDef.filterOperator || '=').toLowerCase();
123
+ const columnRef = adapter.translateColumn(searchDef.actualField || field)
124
+ const operator = (searchDef.filterOperator || '=').toLowerCase()
125
125
  const normalizedValues = normalizeFilterValues(rawValue, searchDef, {
126
126
  isRelationship: Boolean(searchDef.isRelationship),
127
- });
128
- const primaryValue = normalizedValues[0];
127
+ })
128
+ const primaryValue = normalizedValues[0]
129
129
  switch (operator) {
130
130
  case 'like':
131
- query.whereRaw(`${columnRef} like ?`, [`%${primaryValue}%`]);
132
- break;
131
+ query.whereRaw(`${columnRef} like ?`, [`%${primaryValue}%`])
132
+ break
133
133
  case 'in':
134
134
  if (normalizedValues.includes(null) && normalizedValues.length === 1) {
135
- query.whereNull(columnRef);
135
+ query.whereNull(columnRef)
136
136
  } else if (normalizedValues.length > 1) {
137
- query.whereIn(columnRef, normalizedValues);
137
+ query.whereIn(columnRef, normalizedValues)
138
138
  } else if (primaryValue === null) {
139
- query.whereNull(columnRef);
139
+ query.whereNull(columnRef)
140
140
  } else {
141
- query.whereRaw(`${columnRef} = ?`, [primaryValue]);
141
+ query.whereRaw(`${columnRef} = ?`, [primaryValue])
142
142
  }
143
- break;
143
+ break
144
144
  case 'between':
145
145
  if (normalizedValues.length === 2) {
146
- query.whereBetween(columnRef, normalizedValues);
146
+ query.whereBetween(columnRef, normalizedValues)
147
147
  } else if (primaryValue === null) {
148
- query.whereNull(columnRef);
148
+ query.whereNull(columnRef)
149
149
  } else {
150
- query.whereRaw(`${columnRef} = ?`, [primaryValue]);
150
+ query.whereRaw(`${columnRef} = ?`, [primaryValue])
151
151
  }
152
- break;
152
+ break
153
153
  default:
154
154
  if (primaryValue === null) {
155
- query.whereNull(columnRef);
155
+ query.whereNull(columnRef)
156
156
  } else {
157
- query.whereRaw(`${columnRef} ${operator || '='} ?`, [primaryValue]);
157
+ query.whereRaw(`${columnRef} ${operator || '='} ?`, [primaryValue])
158
158
  }
159
- break;
159
+ break
160
160
  }
161
- continue;
161
+ continue
162
162
  }
163
163
  // Leave advanced search fields to downstream hooks
164
- continue;
164
+ continue
165
165
  }
166
- const fieldInfo = ensureFilterableField(descriptor, field);
166
+ const fieldInfo = ensureFilterableField(descriptor, field)
167
167
 
168
168
  const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
169
169
  isRelationship: fieldInfo.isRelationship,
170
- });
170
+ })
171
171
 
172
- const nonNullValues = values.filter((value) => value !== null);
173
- const hasNull = values.length !== nonNullValues.length;
172
+ const nonNullValues = values.filter((value) => value !== null)
173
+ const hasNull = values.length !== nonNullValues.length
174
174
 
175
175
  if (nonNullValues.length === 0 && hasNull) {
176
- query.whereNull(fieldInfo.column);
177
- continue;
176
+ query.whereNull(fieldInfo.column)
177
+ continue
178
178
  }
179
179
 
180
180
  if (nonNullValues.length === 1 && !hasNull) {
181
- query.where(fieldInfo.column, nonNullValues[0]);
182
- continue;
181
+ query.where(fieldInfo.column, nonNullValues[0])
182
+ continue
183
183
  }
184
184
 
185
185
  if (nonNullValues.length > 1 && !hasNull) {
186
- query.whereIn(fieldInfo.column, nonNullValues);
187
- continue;
186
+ query.whereIn(fieldInfo.column, nonNullValues)
187
+ continue
188
188
  }
189
189
 
190
190
  if (nonNullValues.length > 0 && hasNull) {
191
- query.where(function filterGroup() {
192
- this.whereIn(fieldInfo.column, nonNullValues).orWhereNull(fieldInfo.column);
193
- });
191
+ query.where(function filterGroup () {
192
+ this.whereIn(fieldInfo.column, nonNullValues).orWhereNull(fieldInfo.column)
193
+ })
194
194
  }
195
195
  }
196
- };
196
+ }
197
197
 
198
198
  const applySortingToQuery = ({ query, sort, descriptor, scope }) => {
199
- let sortList = Array.isArray(sort) ? sort : (sort ? [sort] : []);
199
+ let sortList = Array.isArray(sort) ? sort : (sort ? [sort] : [])
200
200
 
201
201
  if (sortList.length === 0 && scope?.vars?.defaultSort) {
202
- const defaultSort = scope.vars.defaultSort;
202
+ const defaultSort = scope.vars.defaultSort
203
203
  if (Array.isArray(defaultSort)) {
204
- sortList = defaultSort;
204
+ sortList = defaultSort
205
205
  } else if (typeof defaultSort === 'string') {
206
- sortList = [defaultSort];
206
+ sortList = [defaultSort]
207
207
  } else if (defaultSort && typeof defaultSort === 'object') {
208
- const field = defaultSort.field || defaultSort.column || 'id';
209
- const direction = (defaultSort.direction || '').toLowerCase() === 'desc' ? '-' : '';
210
- sortList = [`${direction}${field}`];
208
+ const field = defaultSort.field || defaultSort.column || 'id'
209
+ const direction = (defaultSort.direction || '').toLowerCase() === 'desc' ? '-' : ''
210
+ sortList = [`${direction}${field}`]
211
211
  }
212
212
  }
213
213
 
214
- const effectiveSort = sortList.length > 0 ? sortList : ['id'];
215
- const descriptors = [];
214
+ const effectiveSort = sortList.length > 0 ? sortList : ['id']
215
+ const descriptors = []
216
216
 
217
217
  for (const entry of effectiveSort) {
218
- const desc = typeof entry === 'string' && entry.startsWith('-');
219
- const field = typeof entry === 'string' ? (desc ? entry.slice(1) : entry) : entry;
220
- const fieldInfo = resolveFieldInfo(descriptor, field);
221
- if (!fieldInfo?.column) continue;
222
- query.orderBy(fieldInfo.column, desc ? 'desc' : 'asc');
218
+ const desc = typeof entry === 'string' && entry.startsWith('-')
219
+ const field = typeof entry === 'string' ? (desc ? entry.slice(1) : entry) : entry
220
+ const fieldInfo = resolveFieldInfo(descriptor, field)
221
+ if (!fieldInfo?.column) continue
222
+ query.orderBy(fieldInfo.column, desc ? 'desc' : 'asc')
223
223
  descriptors.push({
224
224
  field,
225
225
  column: fieldInfo.column,
226
226
  direction: desc ? 'desc' : 'asc',
227
227
  definition: fieldInfo.definition || null,
228
228
  isRelationship: fieldInfo.isRelationship || false,
229
- });
229
+ })
230
230
  }
231
231
 
232
232
  if (descriptors.length === 0) {
@@ -236,39 +236,39 @@ export const RestApiAnyapiKnexPlugin = {
236
236
  direction: 'asc',
237
237
  definition: { type: 'id' },
238
238
  isRelationship: false,
239
- });
239
+ })
240
240
  }
241
241
 
242
- return descriptors;
243
- };
242
+ return descriptors
243
+ }
244
244
 
245
245
  const buildQueryString = (queryParams = {}) => {
246
246
  const buildParts = (prefix, value) => {
247
- const parts = [];
247
+ const parts = []
248
248
  if (Array.isArray(value)) {
249
- if (value.length === 0) return parts;
250
- parts.push(`${prefix}=${value.map((item) => encodeURIComponent(item)).join(',')}`);
251
- return parts;
249
+ if (value.length === 0) return parts
250
+ parts.push(`${prefix}=${value.map((item) => encodeURIComponent(item)).join(',')}`)
251
+ return parts
252
252
  }
253
253
  if (value && typeof value === 'object') {
254
254
  for (const [key, subValue] of Object.entries(value)) {
255
- if (subValue === undefined || subValue === null) continue;
256
- const newPrefix = `${prefix}[${key}]`;
257
- parts.push(...buildParts(newPrefix, subValue));
255
+ if (subValue === undefined || subValue === null) continue
256
+ const newPrefix = `${prefix}[${key}]`
257
+ parts.push(...buildParts(newPrefix, subValue))
258
258
  }
259
- return parts;
259
+ return parts
260
260
  }
261
- if (value === undefined || value === null) return parts;
262
- parts.push(`${prefix}=${encodeURIComponent(value)}`);
263
- return parts;
264
- };
261
+ if (value === undefined || value === null) return parts
262
+ parts.push(`${prefix}=${encodeURIComponent(value)}`)
263
+ return parts
264
+ }
265
265
 
266
- const parts = [];
266
+ const parts = []
267
267
  for (const [key, value] of Object.entries(queryParams)) {
268
- parts.push(...buildParts(key, value));
268
+ parts.push(...buildParts(key, value))
269
269
  }
270
- return parts.length > 0 ? `?${parts.join('&')}` : '';
271
- };
270
+ return parts.length > 0 ? `?${parts.join('&')}` : ''
271
+ }
272
272
 
273
273
  const applyPaginationToQuery = ({
274
274
  query,
@@ -277,10 +277,10 @@ export const RestApiAnyapiKnexPlugin = {
277
277
  sortDescriptors,
278
278
  adapter,
279
279
  }) => {
280
- const pageParams = queryParams?.page || {};
281
- const scopeVars = scope?.vars || {};
282
- const defaultLimit = scopeVars.queryDefaultLimit || DEFAULT_QUERY_LIMIT;
283
- const maxLimit = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT;
280
+ const pageParams = queryParams?.page || {}
281
+ const scopeVars = scope?.vars || {}
282
+ const defaultLimit = scopeVars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
283
+ const maxLimit = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
284
284
 
285
285
  const descriptors = (sortDescriptors && sortDescriptors.length > 0)
286
286
  ? sortDescriptors
@@ -290,15 +290,15 @@ export const RestApiAnyapiKnexPlugin = {
290
290
  direction: 'asc',
291
291
  definition: { type: 'id' },
292
292
  isRelationship: false,
293
- }];
293
+ }]
294
294
 
295
- const hasCursorParam = pageParams.after !== undefined || pageParams.before !== undefined;
296
- const hasPageSize = pageParams.size !== undefined;
297
- const hasPageNumber = pageParams.number !== undefined;
295
+ const hasCursorParam = pageParams.after !== undefined || pageParams.before !== undefined
296
+ const hasPageSize = pageParams.size !== undefined
297
+ const hasPageNumber = pageParams.number !== undefined
298
298
 
299
299
  const parseCursorOrThrow = (rawCursor, paramName) => {
300
300
  try {
301
- return parseCursor(rawCursor);
301
+ return parseCursor(rawCursor)
302
302
  } catch (error) {
303
303
  throw new RestApiValidationError(
304
304
  `Invalid cursor format in ${paramName} parameter`,
@@ -309,88 +309,88 @@ export const RestApiAnyapiKnexPlugin = {
309
309
  rule: 'invalid_cursor',
310
310
  message: error.message,
311
311
  }],
312
- },
313
- );
312
+ }
313
+ )
314
314
  }
315
- };
315
+ }
316
316
 
317
317
  const coerceCursorValues = (cursorMap) => {
318
- const typed = {};
318
+ const typed = {}
319
319
  for (const descriptor of descriptors) {
320
- if (descriptor.field === undefined) continue;
321
- const raw = cursorMap[descriptor.field];
322
- if (raw === undefined) continue;
320
+ if (descriptor.field === undefined) continue
321
+ const raw = cursorMap[descriptor.field]
322
+ if (raw === undefined) continue
323
323
  typed[descriptor.field] = coerceValueForDefinition(
324
324
  raw,
325
325
  descriptor.definition,
326
- { isRelationship: descriptor.isRelationship },
327
- );
326
+ { isRelationship: descriptor.isRelationship }
327
+ )
328
328
  }
329
- return typed;
330
- };
329
+ return typed
330
+ }
331
331
 
332
332
  const buildCursorPredicate = (cursorValues, operatorSelector) => {
333
333
  if (!descriptors || descriptors.length === 0) {
334
- return;
334
+ return
335
335
  }
336
336
 
337
337
  if (descriptors.length === 1) {
338
- const descriptor = descriptors[0];
339
- const value = cursorValues[descriptor.field];
340
- if (value === undefined) return;
341
- const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column;
342
- const operator = operatorSelector(descriptor.direction);
338
+ const descriptor = descriptors[0]
339
+ const value = cursorValues[descriptor.field]
340
+ if (value === undefined) return
341
+ const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
342
+ const operator = operatorSelector(descriptor.direction)
343
343
  if (adapter) {
344
- query.whereRaw(`${columnRef} ${operator} ?`, [value]);
344
+ query.whereRaw(`${columnRef} ${operator} ?`, [value])
345
345
  } else {
346
- query.where(columnRef, operator, value);
346
+ query.where(columnRef, operator, value)
347
347
  }
348
- return;
348
+ return
349
349
  }
350
350
 
351
- const translatedConditions = [];
351
+ const translatedConditions = []
352
352
 
353
353
  descriptors.forEach((descriptor, index) => {
354
- const value = cursorValues[descriptor.field];
355
- if (value === undefined) return;
354
+ const value = cursorValues[descriptor.field]
355
+ if (value === undefined) return
356
356
 
357
- const chain = [];
357
+ const chain = []
358
358
  for (let i = 0; i < index; i += 1) {
359
- const prev = descriptors[i];
360
- const prevValue = cursorValues[prev.field];
361
- if (prevValue === undefined) return;
362
- const prevColumn = adapter ? adapter.translateColumn(prev.column) : prev.column;
363
- chain.push({ column: prevColumn, operator: '=', value: prevValue });
359
+ const prev = descriptors[i]
360
+ const prevValue = cursorValues[prev.field]
361
+ if (prevValue === undefined) return
362
+ const prevColumn = adapter ? adapter.translateColumn(prev.column) : prev.column
363
+ chain.push({ column: prevColumn, operator: '=', value: prevValue })
364
364
  }
365
365
 
366
- const operator = operatorSelector(descriptor.direction);
367
- const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column;
368
- chain.push({ column: columnRef, operator, value });
369
- translatedConditions.push(chain);
370
- });
366
+ const operator = operatorSelector(descriptor.direction)
367
+ const columnRef = adapter ? adapter.translateColumn(descriptor.column) : descriptor.column
368
+ chain.push({ column: columnRef, operator, value })
369
+ translatedConditions.push(chain)
370
+ })
371
371
 
372
372
  if (translatedConditions.length === 0) {
373
- return;
373
+ return
374
374
  }
375
375
 
376
- const predicateParts = [];
377
- const predicateBindings = [];
376
+ const predicateParts = []
377
+ const predicateBindings = []
378
378
  translatedConditions.forEach((chain) => {
379
379
  const chainParts = chain.map(({ column, operator, value }) => {
380
- predicateBindings.push(value);
381
- return `${column} ${operator} ?`;
382
- });
383
- predicateParts.push(`(${chainParts.join(' AND ')})`);
384
- });
380
+ predicateBindings.push(value)
381
+ return `${column} ${operator} ?`
382
+ })
383
+ predicateParts.push(`(${chainParts.join(' AND ')})`)
384
+ })
385
385
 
386
386
  if (predicateParts.length > 0) {
387
- const combined = predicateParts.join(' OR ');
388
- query.whereRaw(`(${combined})`, predicateBindings);
387
+ const combined = predicateParts.join(' OR ')
388
+ query.whereRaw(`(${combined})`, predicateBindings)
389
389
  }
390
- };
390
+ }
391
391
 
392
392
  const ensurePageSize = () => {
393
- const requestedSize = Number(pageParams.size ?? defaultLimit);
393
+ const requestedSize = Number(pageParams.size ?? defaultLimit)
394
394
  if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
395
395
  throw new RestApiValidationError('Page size must be greater than 0', {
396
396
  fields: ['page.size'],
@@ -399,10 +399,10 @@ export const RestApiAnyapiKnexPlugin = {
399
399
  rule: 'min_value',
400
400
  message: 'Page size must be a positive number',
401
401
  }],
402
- });
402
+ })
403
403
  }
404
- return Math.min(Math.trunc(requestedSize), maxLimit);
405
- };
404
+ return Math.min(Math.trunc(requestedSize), maxLimit)
405
+ }
406
406
 
407
407
  if (hasCursorParam || (hasPageSize && !hasPageNumber)) {
408
408
  if (pageParams.after && pageParams.before) {
@@ -415,54 +415,54 @@ export const RestApiAnyapiKnexPlugin = {
415
415
  rule: 'conflict',
416
416
  message: 'Provide either page[after] or page[before], not both',
417
417
  }],
418
- },
419
- );
418
+ }
419
+ )
420
420
  }
421
421
 
422
- const pageSize = ensurePageSize();
423
- query.limit(pageSize + 1);
424
-
425
- if (pageParams.after) {
426
- const cursorMap = parseCursorOrThrow(pageParams.after, 'after');
427
- const cursorValues = coerceCursorValues(cursorMap);
428
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'));
429
- } else if (pageParams.before) {
430
- const cursorMap = parseCursorOrThrow(pageParams.before, 'before');
431
- const cursorValues = coerceCursorValues(cursorMap);
432
- buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'));
433
- }
422
+ const pageSize = ensurePageSize()
423
+ query.limit(pageSize + 1)
424
+
425
+ if (pageParams.after) {
426
+ const cursorMap = parseCursorOrThrow(pageParams.after, 'after')
427
+ const cursorValues = coerceCursorValues(cursorMap)
428
+ buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
429
+ } else if (pageParams.before) {
430
+ const cursorMap = parseCursorOrThrow(pageParams.before, 'before')
431
+ const cursorValues = coerceCursorValues(cursorMap)
432
+ buildCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
433
+ }
434
434
 
435
435
  return {
436
436
  mode: 'cursor',
437
437
  pageSize,
438
438
  sortDescriptors: descriptors,
439
- };
439
+ }
440
440
  }
441
441
 
442
442
  if (hasPageSize || hasPageNumber) {
443
- const pageSize = ensurePageSize();
444
- const pageNumber = Math.trunc(Number(pageParams.number ?? 1));
445
- const safePageNumber = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1;
446
- const offset = (safePageNumber - 1) * pageSize;
443
+ const pageSize = ensurePageSize()
444
+ const pageNumber = Math.trunc(Number(pageParams.number ?? 1))
445
+ const safePageNumber = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1
446
+ const offset = (safePageNumber - 1) * pageSize
447
447
 
448
- query.limit(pageSize).offset(offset);
448
+ query.limit(pageSize).offset(offset)
449
449
 
450
450
  return {
451
451
  mode: 'offset',
452
452
  page: safePageNumber,
453
453
  pageSize,
454
454
  sortDescriptors: descriptors,
455
- };
455
+ }
456
456
  }
457
457
 
458
- query.limit(defaultLimit);
458
+ query.limit(defaultLimit)
459
459
  return {
460
460
  mode: 'default',
461
461
  page: 1,
462
462
  pageSize: defaultLimit,
463
463
  sortDescriptors: descriptors,
464
- };
465
- };
464
+ }
465
+ }
466
466
 
467
467
  const canonicalizeLinkPair = ({
468
468
  tenantId,
@@ -473,11 +473,11 @@ export const RestApiAnyapiKnexPlugin = {
473
473
  rightResource,
474
474
  rightId,
475
475
  }) => {
476
- const normalizedLeftId = normalizeId(leftId);
477
- const normalizedRightId = normalizeId(rightId);
476
+ const normalizedLeftId = normalizeId(leftId)
477
+ const normalizedRightId = normalizeId(rightId)
478
478
 
479
479
  if (normalizedLeftId == null || normalizedRightId == null) {
480
- return null;
480
+ return null
481
481
  }
482
482
 
483
483
  if (inverseRelationshipKey && inverseRelationshipKey < relationshipKey) {
@@ -489,7 +489,7 @@ export const RestApiAnyapiKnexPlugin = {
489
489
  left_id: normalizedRightId,
490
490
  right_resource: leftResource,
491
491
  right_id: normalizedLeftId,
492
- };
492
+ }
493
493
  }
494
494
 
495
495
  return {
@@ -500,8 +500,8 @@ export const RestApiAnyapiKnexPlugin = {
500
500
  left_id: normalizedLeftId,
501
501
  right_resource: rightResource,
502
502
  right_id: normalizedRightId,
503
- };
504
- };
503
+ }
504
+ }
505
505
 
506
506
  const buildLinkIdentity = (canonicalRow) => ({
507
507
  tenant_id: canonicalRow.tenant_id,
@@ -510,63 +510,63 @@ export const RestApiAnyapiKnexPlugin = {
510
510
  left_id: canonicalRow.left_id,
511
511
  right_resource: canonicalRow.right_resource,
512
512
  right_id: canonicalRow.right_id,
513
- });
513
+ })
514
514
 
515
515
  const findInverseManyToMany = async ({ descriptor, relInfo }) => {
516
- if (!relInfo?.target) return null;
516
+ if (!relInfo?.target) return null
517
517
 
518
- let targetDescriptor;
518
+ let targetDescriptor
519
519
  try {
520
- targetDescriptor = await getDescriptor(relInfo.target);
520
+ targetDescriptor = await getDescriptor(relInfo.target)
521
521
  } catch (error) {
522
- return null;
522
+ return null
523
523
  }
524
524
 
525
- const entries = Object.entries(targetDescriptor.manyToMany || {});
525
+ const entries = Object.entries(targetDescriptor.manyToMany || {})
526
526
  for (const [candidateName, candidateInfo] of entries) {
527
- const candidateTarget = candidateInfo.target || descriptor.resource;
528
- if (candidateTarget !== descriptor.resource) continue;
527
+ const candidateTarget = candidateInfo.target || descriptor.resource
528
+ if (candidateTarget !== descriptor.resource) continue
529
529
 
530
530
  if (relInfo.through && candidateInfo.through && relInfo.through !== candidateInfo.through) {
531
- continue;
531
+ continue
532
532
  }
533
533
 
534
534
  if (
535
535
  relInfo.foreignKey && relInfo.otherKey &&
536
536
  candidateInfo.foreignKey && candidateInfo.otherKey
537
537
  ) {
538
- const foreignMatches = relInfo.foreignKey === candidateInfo.otherKey;
539
- const otherMatches = relInfo.otherKey === candidateInfo.foreignKey;
540
- if (!foreignMatches || !otherMatches) continue;
538
+ const foreignMatches = relInfo.foreignKey === candidateInfo.otherKey
539
+ const otherMatches = relInfo.otherKey === candidateInfo.foreignKey
540
+ if (!foreignMatches || !otherMatches) continue
541
541
  }
542
542
 
543
- const relationshipKey = candidateInfo.relationship
544
- || `${targetDescriptor.tenant}:${targetDescriptor.resource}:${candidateName}`;
543
+ const relationshipKey = candidateInfo.relationship ||
544
+ `${targetDescriptor.tenant}:${targetDescriptor.resource}:${candidateName}`
545
545
 
546
546
  return {
547
547
  descriptor: targetDescriptor,
548
548
  relName: candidateName,
549
549
  relInfo: candidateInfo,
550
550
  relationshipKey,
551
- };
551
+ }
552
552
  }
553
553
 
554
- return null;
555
- };
554
+ return null
555
+ }
556
556
 
557
557
  const getManyToManyInfo = async (scopeName, relName) => {
558
- const descriptor = await getDescriptor(scopeName);
559
- const relInfo = descriptor.manyToMany?.[relName];
560
- if (!relInfo) return null;
561
- const relationshipKey = relInfo.relationship || `${descriptor.tenant}:${descriptor.resource}:${relName}`;
562
- const inverse = await findInverseManyToMany({ descriptor, relInfo });
563
- const inverseRelationshipKey = inverse?.relationshipKey || null;
564
- return { descriptor, relInfo, relationshipKey, inverseRelationshipKey };
565
- };
558
+ const descriptor = await getDescriptor(scopeName)
559
+ const relInfo = descriptor.manyToMany?.[relName]
560
+ if (!relInfo) return null
561
+ const relationshipKey = relInfo.relationship || `${descriptor.tenant}:${descriptor.resource}:${relName}`
562
+ const inverse = await findInverseManyToMany({ descriptor, relInfo })
563
+ const inverseRelationshipKey = inverse?.relationshipKey || null
564
+ return { descriptor, relInfo, relationshipKey, inverseRelationshipKey }
565
+ }
566
566
 
567
567
  const loadLinkRowsForResource = async ({ descriptor, relationshipKey, resourceId, db }) => {
568
- const ownerId = normalizeId(resourceId);
569
- if (ownerId == null) return new Map();
568
+ const ownerId = normalizeId(resourceId)
569
+ if (ownerId == null) return new Map()
570
570
 
571
571
  const rows = await db(LINKS_TABLE)
572
572
  .where('tenant_id', descriptor.tenant)
@@ -575,36 +575,36 @@ export const RestApiAnyapiKnexPlugin = {
575
575
  .where((q) => {
576
576
  q.where('relationship', relationshipKey)
577
577
  .andWhere('left_resource', descriptor.resource)
578
- .andWhere('left_id', ownerId);
578
+ .andWhere('left_id', ownerId)
579
579
  })
580
580
  .orWhere((q) => {
581
581
  q.where('inverse_relationship', relationshipKey)
582
582
  .andWhere('right_resource', descriptor.resource)
583
- .andWhere('right_id', ownerId);
584
- });
583
+ .andWhere('right_id', ownerId)
584
+ })
585
585
  })
586
- .select('id', 'relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id');
586
+ .select('id', 'relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
587
587
 
588
- const map = new Map();
588
+ const map = new Map()
589
589
 
590
590
  for (const row of rows) {
591
- let relatedResource;
592
- let relatedId;
593
- let otherKey = null;
591
+ let relatedResource
592
+ let relatedId
593
+ let otherKey = null
594
594
 
595
595
  if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
596
- relatedResource = row.right_resource;
597
- relatedId = normalizeId(row.right_id);
598
- otherKey = row.inverse_relationship || null;
596
+ relatedResource = row.right_resource
597
+ relatedId = normalizeId(row.right_id)
598
+ otherKey = row.inverse_relationship || null
599
599
  } else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
600
- relatedResource = row.left_resource;
601
- relatedId = normalizeId(row.left_id);
602
- otherKey = row.relationship || null;
600
+ relatedResource = row.left_resource
601
+ relatedId = normalizeId(row.left_id)
602
+ otherKey = row.relationship || null
603
603
  } else {
604
- continue;
604
+ continue
605
605
  }
606
606
 
607
- if (relatedId == null) continue;
607
+ if (relatedId == null) continue
608
608
 
609
609
  const canonical = canonicalizeLinkPair({
610
610
  tenantId: descriptor.tenant,
@@ -614,27 +614,27 @@ export const RestApiAnyapiKnexPlugin = {
614
614
  leftId: ownerId,
615
615
  rightResource: relatedResource,
616
616
  rightId: relatedId,
617
- });
617
+ })
618
618
 
619
- if (!canonical) continue;
619
+ if (!canonical) continue
620
620
 
621
621
  map.set(relatedId, {
622
622
  rowId: row.id,
623
623
  relatedResource,
624
624
  canonical,
625
625
  otherKey,
626
- });
626
+ })
627
627
  }
628
628
 
629
- return map;
630
- };
629
+ return map
630
+ }
631
631
 
632
632
  const fetchLinksForParents = async ({ descriptor, relationshipKey, parentIds, db }) => {
633
633
  const normalizedIds = parentIds
634
634
  .map((id) => normalizeId(id))
635
- .filter((id) => id !== null);
635
+ .filter((id) => id !== null)
636
636
 
637
- if (normalizedIds.length === 0) return [];
637
+ if (normalizedIds.length === 0) return []
638
638
 
639
639
  const rows = await db(LINKS_TABLE)
640
640
  .where('tenant_id', descriptor.tenant)
@@ -643,68 +643,68 @@ export const RestApiAnyapiKnexPlugin = {
643
643
  .where((q) => {
644
644
  q.where('relationship', relationshipKey)
645
645
  .andWhere('left_resource', descriptor.resource)
646
- .whereIn('left_id', normalizedIds);
646
+ .whereIn('left_id', normalizedIds)
647
647
  })
648
648
  .orWhere((q) => {
649
649
  q.where('inverse_relationship', relationshipKey)
650
650
  .andWhere('right_resource', descriptor.resource)
651
- .whereIn('right_id', normalizedIds);
652
- });
651
+ .whereIn('right_id', normalizedIds)
652
+ })
653
653
  })
654
- .select('relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id');
654
+ .select('relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
655
655
 
656
- const results = [];
656
+ const results = []
657
657
 
658
658
  for (const row of rows) {
659
- let parentId;
660
- let childId;
661
- let childType;
659
+ let parentId
660
+ let childId
661
+ let childType
662
662
 
663
663
  if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
664
- parentId = normalizeId(row.left_id);
665
- childId = normalizeId(row.right_id);
666
- childType = row.right_resource;
664
+ parentId = normalizeId(row.left_id)
665
+ childId = normalizeId(row.right_id)
666
+ childType = row.right_resource
667
667
  } else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
668
- parentId = normalizeId(row.right_id);
669
- childId = normalizeId(row.left_id);
670
- childType = row.left_resource;
668
+ parentId = normalizeId(row.right_id)
669
+ childId = normalizeId(row.left_id)
670
+ childType = row.left_resource
671
671
  } else {
672
- continue;
672
+ continue
673
673
  }
674
674
 
675
- if (parentId == null || childId == null) continue;
675
+ if (parentId == null || childId == null) continue
676
676
 
677
- results.push({ parentId, childId, childType });
677
+ results.push({ parentId, childId, childType })
678
678
  }
679
679
 
680
- return results;
681
- };
680
+ return results
681
+ }
682
682
 
683
683
  const ensureTargetsExist = async ({ relInfo, relData, transaction }) => {
684
- const relArray = Array.isArray(relData) ? relData : [];
685
- if (relArray.length === 0) return;
686
- const targetScope = api.resources[relInfo.target];
684
+ const relArray = Array.isArray(relData) ? relData : []
685
+ if (relArray.length === 0) return
686
+ const targetScope = api.resources[relInfo.target]
687
687
  if (!targetScope) {
688
- throw new Error(`Target resource '${relInfo.target}' not found`);
688
+ throw new Error(`Target resource '${relInfo.target}' not found`)
689
689
  }
690
690
  for (const identifier of relArray) {
691
691
  if (!identifier?.id) {
692
- throw new Error('Relationship data requires resource identifier with id');
692
+ throw new Error('Relationship data requires resource identifier with id')
693
693
  }
694
694
  if (identifier.type && identifier.type !== relInfo.target) {
695
- throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`);
695
+ throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`)
696
696
  }
697
- await targetScope.get({ id: identifier.id, transaction, simplified: false });
697
+ await targetScope.get({ id: identifier.id, transaction, simplified: false })
698
698
  }
699
- };
699
+ }
700
700
 
701
701
  const attachLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
702
- const relArray = Array.isArray(relData) ? relData : [];
703
- if (relArray.length === 0) return;
702
+ const relArray = Array.isArray(relData) ? relData : []
703
+ if (relArray.length === 0) return
704
704
 
705
705
  for (const identifier of relArray) {
706
- const rightId = normalizeId(identifier.id);
707
- if (rightId == null) continue;
706
+ const rightId = normalizeId(identifier.id)
707
+ if (rightId == null) continue
708
708
 
709
709
  const canonical = canonicalizeLinkPair({
710
710
  tenantId: descriptor.tenant,
@@ -714,15 +714,15 @@ export const RestApiAnyapiKnexPlugin = {
714
714
  leftId,
715
715
  rightResource: relInfo.target,
716
716
  rightId,
717
- });
717
+ })
718
718
 
719
- if (!canonical) continue;
719
+ if (!canonical) continue
720
720
 
721
- const identity = buildLinkIdentity(canonical);
721
+ const identity = buildLinkIdentity(canonical)
722
722
 
723
723
  const existing = await db(LINKS_TABLE)
724
724
  .where(identity)
725
- .first();
725
+ .first()
726
726
 
727
727
  if (existing) {
728
728
  if (!existing.inverse_relationship && canonical.inverse_relationship) {
@@ -731,9 +731,9 @@ export const RestApiAnyapiKnexPlugin = {
731
731
  .update({
732
732
  inverse_relationship: canonical.inverse_relationship,
733
733
  updated_at: db.fn.now(),
734
- });
734
+ })
735
735
  }
736
- continue;
736
+ continue
737
737
  }
738
738
 
739
739
  await db(LINKS_TABLE).insert({
@@ -741,19 +741,19 @@ export const RestApiAnyapiKnexPlugin = {
741
741
  payload: null,
742
742
  created_at: db.fn.now(),
743
743
  updated_at: db.fn.now(),
744
- });
744
+ })
745
745
  }
746
- };
746
+ }
747
747
 
748
748
  api.anyapi.links = {
749
749
  attachMany: async ({ context, scopeName, relName, relDef, relData }) => {
750
- const info = await getManyToManyInfo(scopeName, relName);
750
+ const info = await getManyToManyInfo(scopeName, relName)
751
751
  if (!info) {
752
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`);
752
+ throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
753
753
  }
754
- const db = context.transaction || context.db || api.knex.instance;
755
- const leftId = normalizeId(context.id);
756
- await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction });
754
+ const db = context.transaction || context.db || api.knex.instance
755
+ const leftId = normalizeId(context.id)
756
+ await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction })
757
757
  await attachLinks({
758
758
  descriptor: info.descriptor,
759
759
  relInfo: info.relInfo,
@@ -762,16 +762,16 @@ export const RestApiAnyapiKnexPlugin = {
762
762
  leftId,
763
763
  relData,
764
764
  db,
765
- });
765
+ })
766
766
  },
767
767
  syncMany: async ({ context, scopeName, relName, relDef, relData, isUpdate }) => {
768
- const info = await getManyToManyInfo(scopeName, relName);
768
+ const info = await getManyToManyInfo(scopeName, relName)
769
769
  if (!info) {
770
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`);
770
+ throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
771
771
  }
772
- const db = context.transaction || context.db || api.knex.instance;
773
- const leftId = normalizeId(context.id);
774
- await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction });
772
+ const db = context.transaction || context.db || api.knex.instance
773
+ const leftId = normalizeId(context.id)
774
+ await ensureTargetsExist({ relInfo: info.relInfo, relData, transaction: context.transaction })
775
775
  if (isUpdate) {
776
776
  await syncLinks({
777
777
  descriptor: info.descriptor,
@@ -781,7 +781,7 @@ export const RestApiAnyapiKnexPlugin = {
781
781
  leftId,
782
782
  relData,
783
783
  db,
784
- });
784
+ })
785
785
  } else {
786
786
  await attachLinks({
787
787
  descriptor: info.descriptor,
@@ -791,16 +791,16 @@ export const RestApiAnyapiKnexPlugin = {
791
791
  leftId,
792
792
  relData,
793
793
  db,
794
- });
794
+ })
795
795
  }
796
796
  },
797
797
  removeMany: async ({ context, scopeName, relName, relData }) => {
798
- const info = await getManyToManyInfo(scopeName, relName);
798
+ const info = await getManyToManyInfo(scopeName, relName)
799
799
  if (!info) {
800
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`);
800
+ throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
801
801
  }
802
- const db = context.transaction || context.db || api.knex.instance;
803
- const leftId = normalizeId(context.id);
802
+ const db = context.transaction || context.db || api.knex.instance
803
+ const leftId = normalizeId(context.id)
804
804
  await removeLinks({
805
805
  descriptor: info.descriptor,
806
806
  relInfo: info.relInfo,
@@ -809,15 +809,15 @@ export const RestApiAnyapiKnexPlugin = {
809
809
  leftId,
810
810
  relData,
811
811
  db,
812
- });
812
+ })
813
813
  },
814
814
  listMany: async ({ context, scopeName, relName }) => {
815
- const info = await getManyToManyInfo(scopeName, relName);
815
+ const info = await getManyToManyInfo(scopeName, relName)
816
816
  if (!info) {
817
- throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`);
817
+ throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
818
818
  }
819
- const db = context.transaction || context.db || api.knex.instance;
820
- const leftId = normalizeId(context.id);
819
+ const db = context.transaction || context.db || api.knex.instance
820
+ const leftId = normalizeId(context.id)
821
821
  return listLinks({
822
822
  descriptor: info.descriptor,
823
823
  relInfo: info.relInfo,
@@ -825,43 +825,43 @@ export const RestApiAnyapiKnexPlugin = {
825
825
  inverseRelationshipKey: info.inverseRelationshipKey,
826
826
  leftId,
827
827
  db,
828
- });
828
+ })
829
829
  },
830
830
  fetchManyToManyRows: async ({ scopeName, relName, parentIds, context }) => {
831
- const info = await getManyToManyInfo(scopeName, relName);
832
- if (!info) return [];
833
- const db = context.db || context.transaction || api.knex.instance;
831
+ const info = await getManyToManyInfo(scopeName, relName)
832
+ if (!info) return []
833
+ const db = context.db || context.transaction || api.knex.instance
834
834
  return fetchLinksForParents({
835
835
  descriptor: info.descriptor,
836
836
  relationshipKey: info.relationshipKey,
837
837
  parentIds,
838
838
  db,
839
- });
839
+ })
840
840
  },
841
- };
841
+ }
842
842
 
843
843
  const syncLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
844
- const relArray = Array.isArray(relData) ? relData : [];
844
+ const relArray = Array.isArray(relData) ? relData : []
845
845
  const existingMap = await loadLinkRowsForResource({
846
846
  descriptor,
847
847
  relationshipKey,
848
848
  resourceId: leftId,
849
849
  db,
850
- });
850
+ })
851
851
 
852
852
  const desired = new Set(
853
853
  relArray
854
854
  .map((identifier) => normalizeId(identifier.id))
855
- .filter((id) => id !== null),
856
- );
855
+ .filter((id) => id !== null)
856
+ )
857
857
 
858
- const existingIds = new Set(existingMap.keys());
858
+ const existingIds = new Set(existingMap.keys())
859
859
 
860
- const toAdd = [...desired].filter((id) => !existingIds.has(id));
861
- const toRemove = [...existingIds].filter((id) => !desired.has(id));
860
+ const toAdd = [...desired].filter((id) => !existingIds.has(id))
861
+ const toRemove = [...existingIds].filter((id) => !desired.has(id))
862
862
 
863
863
  if (toAdd.length > 0) {
864
- const data = toAdd.map((id) => ({ id }));
864
+ const data = toAdd.map((id) => ({ id }))
865
865
  await attachLinks({
866
866
  descriptor,
867
867
  relInfo,
@@ -870,41 +870,41 @@ export const RestApiAnyapiKnexPlugin = {
870
870
  leftId,
871
871
  relData: data,
872
872
  db,
873
- });
873
+ })
874
874
  }
875
875
 
876
876
  if (toRemove.length > 0) {
877
877
  for (const id of toRemove) {
878
- const entry = existingMap.get(id);
879
- if (!entry?.canonical) continue;
878
+ const entry = existingMap.get(id)
879
+ if (!entry?.canonical) continue
880
880
  await db(LINKS_TABLE)
881
881
  .where(buildLinkIdentity(entry.canonical))
882
- .delete();
882
+ .delete()
883
883
  }
884
884
  }
885
- };
885
+ }
886
886
 
887
887
  const removeLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
888
- const relArray = Array.isArray(relData) ? relData : [];
889
- if (relArray.length === 0) return;
888
+ const relArray = Array.isArray(relData) ? relData : []
889
+ if (relArray.length === 0) return
890
890
 
891
891
  const existingMap = await loadLinkRowsForResource({
892
892
  descriptor,
893
893
  relationshipKey,
894
894
  resourceId: leftId,
895
895
  db,
896
- });
896
+ })
897
897
 
898
898
  for (const identifier of relArray) {
899
- const relatedId = normalizeId(identifier.id);
900
- if (relatedId == null) continue;
899
+ const relatedId = normalizeId(identifier.id)
900
+ if (relatedId == null) continue
901
901
 
902
- const entry = existingMap.get(relatedId);
902
+ const entry = existingMap.get(relatedId)
903
903
  if (entry?.canonical) {
904
904
  await db(LINKS_TABLE)
905
905
  .where(buildLinkIdentity(entry.canonical))
906
- .delete();
907
- continue;
906
+ .delete()
907
+ continue
908
908
  }
909
909
 
910
910
  const canonical = canonicalizeLinkPair({
@@ -915,15 +915,15 @@ export const RestApiAnyapiKnexPlugin = {
915
915
  leftId,
916
916
  rightResource: relInfo.target,
917
917
  rightId: relatedId,
918
- });
918
+ })
919
919
 
920
- if (!canonical) continue;
920
+ if (!canonical) continue
921
921
 
922
922
  await db(LINKS_TABLE)
923
923
  .where(buildLinkIdentity(canonical))
924
- .delete();
924
+ .delete()
925
925
  }
926
- };
926
+ }
927
927
 
928
928
  const listLinks = async ({ descriptor, relInfo, relationshipKey, leftId, db }) => {
929
929
  const existingMap = await loadLinkRowsForResource({
@@ -931,244 +931,244 @@ export const RestApiAnyapiKnexPlugin = {
931
931
  relationshipKey,
932
932
  resourceId: leftId,
933
933
  db,
934
- });
934
+ })
935
935
 
936
936
  return [...existingMap.entries()].map(([relatedId, entry]) => ({
937
937
  type: entry.relatedResource || relInfo.target,
938
938
  id: relatedId,
939
- }));
940
- };
939
+ }))
940
+ }
941
941
 
942
942
  helpers.dataExists = async ({ scopeName, context }) => {
943
- const storageAdapter = getScopeStorageAdapter(scopeName);
943
+ const storageAdapter = getScopeStorageAdapter(scopeName)
944
944
  if (storageAdapter) {
945
- context.storageAdapter = storageAdapter;
945
+ context.storageAdapter = storageAdapter
946
946
  }
947
- const descriptor = await getDescriptor(scopeName);
948
- const { canonical } = descriptor;
949
- const id = context.id;
947
+ const descriptor = await getDescriptor(scopeName)
948
+ const { canonical } = descriptor
949
+ const id = context.id
950
950
 
951
951
  const row = await context.db(canonical.tableName)
952
952
  .select('id')
953
953
  .where(canonical.tenantColumn, descriptor.tenant)
954
954
  .where(canonical.resourceColumn, descriptor.resource)
955
955
  .where('id', id)
956
- .first();
956
+ .first()
957
957
 
958
- return !!row;
959
- };
958
+ return !!row
959
+ }
960
960
 
961
961
  helpers.dataPost = async ({ scopeName, context }) => {
962
- const storageAdapter = getScopeStorageAdapter(scopeName);
962
+ const storageAdapter = getScopeStorageAdapter(scopeName)
963
963
  if (storageAdapter) {
964
- context.storageAdapter = storageAdapter;
964
+ context.storageAdapter = storageAdapter
965
965
  }
966
- const descriptor = await getDescriptor(scopeName);
967
- const { canonical } = descriptor;
968
- const attributes = context.inputRecord?.data?.attributes || {};
969
- const row = translateAttributesForStorage(attributes, descriptor);
970
- row[canonical.tenantColumn] = descriptor.tenant;
971
- row[canonical.resourceColumn] = descriptor.resource;
966
+ const descriptor = await getDescriptor(scopeName)
967
+ const { canonical } = descriptor
968
+ const attributes = context.inputRecord?.data?.attributes || {}
969
+ const row = translateAttributesForStorage(attributes, descriptor)
970
+ row[canonical.tenantColumn] = descriptor.tenant
971
+ row[canonical.resourceColumn] = descriptor.resource
972
972
 
973
973
  const result = await context.db(canonical.tableName)
974
974
  .insert(row)
975
- .returning('id');
975
+ .returning('id')
976
976
 
977
- const inserted = Array.isArray(result) ? result[0] : result;
977
+ const inserted = Array.isArray(result) ? result[0] : result
978
978
  if (inserted && typeof inserted === 'object' && 'id' in inserted) {
979
- return inserted.id;
979
+ return inserted.id
980
980
  }
981
- return inserted;
982
- };
981
+ return inserted
982
+ }
983
983
 
984
984
  helpers.dataPut = async ({ scopeName, context }) => {
985
- const storageAdapter = getScopeStorageAdapter(scopeName);
985
+ const storageAdapter = getScopeStorageAdapter(scopeName)
986
986
  if (storageAdapter) {
987
- context.storageAdapter = storageAdapter;
987
+ context.storageAdapter = storageAdapter
988
988
  }
989
- const descriptor = await getDescriptor(scopeName);
990
- const { canonical } = descriptor;
991
- const id = context.id;
992
- const attributes = context.inputRecord?.data?.attributes || {};
993
- const row = translateAttributesForStorage(attributes, descriptor);
989
+ const descriptor = await getDescriptor(scopeName)
990
+ const { canonical } = descriptor
991
+ const id = context.id
992
+ const attributes = context.inputRecord?.data?.attributes || {}
993
+ const row = translateAttributesForStorage(attributes, descriptor)
994
994
 
995
- row[canonical.resourceColumn] = descriptor.resource;
996
- row[canonical.tenantColumn] = descriptor.tenant;
995
+ row[canonical.resourceColumn] = descriptor.resource
996
+ row[canonical.tenantColumn] = descriptor.tenant
997
997
 
998
998
  const updateRow = Object.fromEntries(
999
999
  Object.entries(row).filter(([, value]) => value !== undefined)
1000
- );
1000
+ )
1001
1001
 
1002
1002
  if (Object.keys(updateRow).length === 0) {
1003
- return 0;
1003
+ return 0
1004
1004
  }
1005
1005
 
1006
1006
  const result = await context.db(canonical.tableName)
1007
1007
  .where('id', id)
1008
1008
  .where(canonical.resourceColumn, descriptor.resource)
1009
1009
  .where(canonical.tenantColumn, descriptor.tenant)
1010
- .update(updateRow);
1010
+ .update(updateRow)
1011
1011
 
1012
- return result;
1013
- };
1012
+ return result
1013
+ }
1014
1014
 
1015
1015
  helpers.dataPatch = async ({ scopeName, context }) => {
1016
- const storageAdapter = getScopeStorageAdapter(scopeName);
1016
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1017
1017
  if (storageAdapter) {
1018
- context.storageAdapter = storageAdapter;
1018
+ context.storageAdapter = storageAdapter
1019
1019
  }
1020
- const descriptor = await getDescriptor(scopeName);
1021
- const { canonical } = descriptor;
1022
- const id = context.id;
1023
- const attributes = context.inputRecord?.data?.attributes || {};
1024
- const row = translateAttributesForStorage(attributes, descriptor);
1020
+ const descriptor = await getDescriptor(scopeName)
1021
+ const { canonical } = descriptor
1022
+ const id = context.id
1023
+ const attributes = context.inputRecord?.data?.attributes || {}
1024
+ const row = translateAttributesForStorage(attributes, descriptor)
1025
1025
 
1026
1026
  const updateRow = Object.fromEntries(
1027
1027
  Object.entries(row).filter(([, value]) => value !== undefined)
1028
- );
1028
+ )
1029
1029
 
1030
1030
  if (Object.keys(updateRow).length === 0) {
1031
- return 0;
1031
+ return 0
1032
1032
  }
1033
1033
 
1034
1034
  const result = await context.db(canonical.tableName)
1035
1035
  .where('id', id)
1036
1036
  .where(canonical.resourceColumn, descriptor.resource)
1037
1037
  .where(canonical.tenantColumn, descriptor.tenant)
1038
- .update(updateRow);
1038
+ .update(updateRow)
1039
1039
 
1040
- return result;
1041
- };
1040
+ return result
1041
+ }
1042
1042
 
1043
1043
  helpers.dataDelete = async ({ scopeName, context }) => {
1044
- const storageAdapter = getScopeStorageAdapter(scopeName);
1044
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1045
1045
  if (storageAdapter) {
1046
- context.storageAdapter = storageAdapter;
1046
+ context.storageAdapter = storageAdapter
1047
1047
  }
1048
- const descriptor = await getDescriptor(scopeName);
1049
- const { canonical } = descriptor;
1050
- const id = context.id;
1048
+ const descriptor = await getDescriptor(scopeName)
1049
+ const { canonical } = descriptor
1050
+ const id = context.id
1051
1051
 
1052
1052
  const result = await context.db(canonical.tableName)
1053
1053
  .where('id', id)
1054
1054
  .where(canonical.resourceColumn, descriptor.resource)
1055
1055
  .where(canonical.tenantColumn, descriptor.tenant)
1056
- .delete();
1056
+ .delete()
1057
1057
 
1058
- return result;
1059
- };
1058
+ return result
1059
+ }
1060
1060
 
1061
1061
  helpers.dataGetMinimal = async ({ scopeName, context }) => {
1062
- const storageAdapter = getScopeStorageAdapter(scopeName);
1062
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1063
1063
  if (storageAdapter) {
1064
- context.storageAdapter = storageAdapter;
1064
+ context.storageAdapter = storageAdapter
1065
1065
  }
1066
- const descriptor = await getDescriptor(scopeName);
1067
- const { canonical } = descriptor;
1068
- const id = context.id;
1069
- const scope = api.resources?.[scopeName];
1066
+ const descriptor = await getDescriptor(scopeName)
1067
+ const { canonical } = descriptor
1068
+ const id = context.id
1069
+ const scope = api.resources?.[scopeName]
1070
1070
 
1071
1071
  const row = await context.db(canonical.tableName)
1072
1072
  .where('id', id)
1073
1073
  .where(canonical.resourceColumn, descriptor.resource)
1074
1074
  .where(canonical.tenantColumn, descriptor.tenant)
1075
- .first();
1075
+ .first()
1076
1076
 
1077
- if (!row) return null;
1078
- const translated = translateRecordFromStorage(row, descriptor);
1077
+ if (!row) return null
1078
+ const translated = translateRecordFromStorage(row, descriptor)
1079
1079
  const minimal = {
1080
1080
  type: descriptor.resource,
1081
1081
  id: String(row.id),
1082
1082
  attributes: translated.attributes,
1083
1083
  relationships: translated.relationships,
1084
- };
1084
+ }
1085
1085
  if (scope) {
1086
1086
  minimal.links = {
1087
1087
  self: buildResourceUrl(context, scope, scopeName, minimal.id),
1088
- };
1088
+ }
1089
1089
  }
1090
- return minimal;
1091
- };
1090
+ return minimal
1091
+ }
1092
1092
 
1093
1093
  const parseIncludeTree = (includeParam) => {
1094
- const tree = {};
1095
- if (!includeParam) return tree;
1094
+ const tree = {}
1095
+ if (!includeParam) return tree
1096
1096
 
1097
1097
  const pushPath = (raw) => {
1098
- if (!raw) return;
1099
- const segments = raw.split('.').map((segment) => segment.trim()).filter(Boolean);
1100
- if (segments.length === 0) return;
1101
- let node = tree;
1098
+ if (!raw) return
1099
+ const segments = raw.split('.').map((segment) => segment.trim()).filter(Boolean)
1100
+ if (segments.length === 0) return
1101
+ let node = tree
1102
1102
  for (const segment of segments) {
1103
- node[segment] = node[segment] || {};
1104
- node = node[segment];
1103
+ node[segment] = node[segment] || {}
1104
+ node = node[segment]
1105
1105
  }
1106
- };
1106
+ }
1107
1107
 
1108
1108
  if (Array.isArray(includeParam)) {
1109
1109
  for (const entry of includeParam) {
1110
- String(entry).split(',').forEach((part) => pushPath(part.trim()));
1110
+ String(entry).split(',').forEach((part) => pushPath(part.trim()))
1111
1111
  }
1112
1112
  } else {
1113
- String(includeParam).split(',').forEach((part) => pushPath(part.trim()));
1113
+ String(includeParam).split(',').forEach((part) => pushPath(part.trim()))
1114
1114
  }
1115
1115
 
1116
- return tree;
1117
- };
1116
+ return tree
1117
+ }
1118
1118
 
1119
1119
  const collectIncludes = async ({ descriptor, resources, includeTree, context, includes, seen }) => {
1120
- const entries = Object.entries(includeTree || {});
1121
- if (entries.length === 0 || !resources || resources.length === 0) return;
1120
+ const entries = Object.entries(includeTree || {})
1121
+ if (entries.length === 0 || !resources || resources.length === 0) return
1122
1122
 
1123
- const db = context.db || context.transaction || api.knex.instance;
1123
+ const db = context.db || context.transaction || api.knex.instance
1124
1124
  const addSelfLink = (resource, resourceType) => {
1125
- const targetScope = api.resources?.[resourceType];
1126
- if (!targetScope) return;
1127
- resource.links = resource.links || {};
1128
- resource.links.self = buildResourceUrl(context, targetScope, resourceType, resource.id);
1129
- };
1125
+ const targetScope = api.resources?.[resourceType]
1126
+ if (!targetScope) return
1127
+ resource.links = resource.links || {}
1128
+ resource.links.self = buildResourceUrl(context, targetScope, resourceType, resource.id)
1129
+ }
1130
1130
 
1131
1131
  for (const [relName, childTree] of entries) {
1132
- const childKeys = Object.keys(childTree || {});
1132
+ const childKeys = Object.keys(childTree || {})
1133
1133
 
1134
- const belongsToInfo = descriptor.belongsTo?.[relName];
1134
+ const belongsToInfo = descriptor.belongsTo?.[relName]
1135
1135
  if (belongsToInfo) {
1136
1136
  const ids = [...new Set(resources
1137
1137
  .map((resource) => resource.relationships?.[relName]?.data?.id)
1138
- .filter((id) => id !== undefined && id !== null))];
1138
+ .filter((id) => id !== undefined && id !== null))]
1139
1139
 
1140
- if (ids.length === 0) continue;
1140
+ if (ids.length === 0) continue
1141
1141
 
1142
- const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target);
1143
- if (!targetDescriptor) continue;
1142
+ const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target)
1143
+ if (!targetDescriptor) continue
1144
1144
 
1145
1145
  const rows = await db(targetDescriptor.canonical.tableName)
1146
1146
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1147
1147
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1148
- .whereIn('id', ids);
1148
+ .whereIn('id', ids)
1149
1149
 
1150
- const newResources = [];
1150
+ const newResources = []
1151
1151
  for (const row of rows) {
1152
- const includeKey = `${targetDescriptor.resource}:${row.id}`;
1153
- if (seen.has(includeKey)) continue;
1154
- seen.add(includeKey);
1155
- const translated = translateRecordFromStorage(row, targetDescriptor);
1152
+ const includeKey = `${targetDescriptor.resource}:${row.id}`
1153
+ if (seen.has(includeKey)) continue
1154
+ seen.add(includeKey)
1155
+ const translated = translateRecordFromStorage(row, targetDescriptor)
1156
1156
  const includeResource = {
1157
1157
  type: targetDescriptor.resource,
1158
1158
  id: String(row.id),
1159
1159
  attributes: translated.attributes,
1160
- };
1160
+ }
1161
1161
  if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1162
- includeResource.relationships = translated.relationships;
1162
+ includeResource.relationships = translated.relationships
1163
1163
  }
1164
- addSelfLink(includeResource, targetDescriptor.resource);
1165
- includes.push(includeResource);
1166
- newResources.push(includeResource);
1164
+ addSelfLink(includeResource, targetDescriptor.resource)
1165
+ includes.push(includeResource)
1166
+ newResources.push(includeResource)
1167
1167
  }
1168
1168
 
1169
1169
  if (childKeys.length > 0 && newResources.length > 0) {
1170
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1171
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1170
+ await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1171
+ await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1172
1172
  await collectIncludes({
1173
1173
  descriptor: targetDescriptor,
1174
1174
  resources: newResources,
@@ -1176,79 +1176,79 @@ export const RestApiAnyapiKnexPlugin = {
1176
1176
  context,
1177
1177
  includes,
1178
1178
  seen,
1179
- });
1179
+ })
1180
1180
  }
1181
- continue;
1181
+ continue
1182
1182
  }
1183
1183
 
1184
- const polymorphicInfo = descriptor.polymorphicBelongsTo?.[relName];
1184
+ const polymorphicInfo = descriptor.polymorphicBelongsTo?.[relName]
1185
1185
  if (polymorphicInfo) {
1186
- const typeToIds = new Map();
1186
+ const typeToIds = new Map()
1187
1187
  for (const resource of resources) {
1188
- const relData = resource.relationships?.[relName]?.data;
1189
- if (!relData?.type || relData?.id == null) continue;
1190
- const targetType = String(relData.type);
1191
- const targetId = normalizeId(relData.id);
1192
- if (targetId == null) continue;
1188
+ const relData = resource.relationships?.[relName]?.data
1189
+ if (!relData?.type || relData?.id == null) continue
1190
+ const targetType = String(relData.type)
1191
+ const targetId = normalizeId(relData.id)
1192
+ if (targetId == null) continue
1193
1193
  if (!typeToIds.has(targetType)) {
1194
- typeToIds.set(targetType, new Set());
1194
+ typeToIds.set(targetType, new Set())
1195
1195
  }
1196
- typeToIds.get(targetType).add(targetId);
1196
+ typeToIds.get(targetType).add(targetId)
1197
1197
  }
1198
1198
 
1199
- const newResources = [];
1199
+ const newResources = []
1200
1200
  for (const [targetType, idSet] of typeToIds.entries()) {
1201
- if (idSet.size === 0) continue;
1202
- let targetDescriptor;
1201
+ if (idSet.size === 0) continue
1202
+ let targetDescriptor
1203
1203
  try {
1204
- targetDescriptor = await getDescriptor(targetType);
1204
+ targetDescriptor = await getDescriptor(targetType)
1205
1205
  } catch (error) {
1206
- continue;
1206
+ continue
1207
1207
  }
1208
1208
 
1209
1209
  const rows = await db(targetDescriptor.canonical.tableName)
1210
1210
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1211
1211
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1212
- .whereIn('id', [...idSet]);
1212
+ .whereIn('id', [...idSet])
1213
1213
 
1214
1214
  for (const row of rows) {
1215
- const includeKey = `${targetDescriptor.resource}:${row.id}`;
1216
- if (seen.has(includeKey)) continue;
1217
- seen.add(includeKey);
1218
- const translated = translateRecordFromStorage(row, targetDescriptor);
1215
+ const includeKey = `${targetDescriptor.resource}:${row.id}`
1216
+ if (seen.has(includeKey)) continue
1217
+ seen.add(includeKey)
1218
+ const translated = translateRecordFromStorage(row, targetDescriptor)
1219
1219
  const includeResource = {
1220
1220
  type: targetDescriptor.resource,
1221
1221
  id: String(row.id),
1222
1222
  attributes: translated.attributes,
1223
- };
1223
+ }
1224
1224
  if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1225
- includeResource.relationships = translated.relationships;
1225
+ includeResource.relationships = translated.relationships
1226
1226
  }
1227
- addSelfLink(includeResource, targetDescriptor.resource);
1228
- includes.push(includeResource);
1229
- newResources.push(includeResource);
1227
+ addSelfLink(includeResource, targetDescriptor.resource)
1228
+ includes.push(includeResource)
1229
+ newResources.push(includeResource)
1230
1230
  }
1231
1231
  }
1232
1232
 
1233
1233
  if (childKeys.length > 0 && newResources.length > 0) {
1234
- const resourcesByType = new Map();
1234
+ const resourcesByType = new Map()
1235
1235
  for (const includeResource of newResources) {
1236
1236
  if (!resourcesByType.has(includeResource.type)) {
1237
- resourcesByType.set(includeResource.type, []);
1237
+ resourcesByType.set(includeResource.type, [])
1238
1238
  }
1239
- resourcesByType.get(includeResource.type).push(includeResource);
1239
+ resourcesByType.get(includeResource.type).push(includeResource)
1240
1240
  }
1241
1241
 
1242
1242
  for (const [targetType, groupedResources] of resourcesByType.entries()) {
1243
- let targetDescriptor;
1243
+ let targetDescriptor
1244
1244
  try {
1245
- targetDescriptor = await getDescriptor(targetType);
1245
+ targetDescriptor = await getDescriptor(targetType)
1246
1246
  } catch (error) {
1247
- continue;
1247
+ continue
1248
1248
  }
1249
1249
 
1250
- await attachHasManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context });
1251
- await attachManyToManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context });
1250
+ await attachHasManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
1251
+ await attachManyToManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
1252
1252
  await collectIncludes({
1253
1253
  descriptor: targetDescriptor,
1254
1254
  resources: groupedResources,
@@ -1256,90 +1256,90 @@ export const RestApiAnyapiKnexPlugin = {
1256
1256
  context,
1257
1257
  includes,
1258
1258
  seen,
1259
- });
1259
+ })
1260
1260
  }
1261
1261
  }
1262
1262
 
1263
- continue;
1263
+ continue
1264
1264
  }
1265
1265
 
1266
- const hasManyInfo = descriptor.relationships?.[relName];
1266
+ const hasManyInfo = descriptor.relationships?.[relName]
1267
1267
  if (hasManyInfo?.type === 'hasMany' && hasManyInfo.target && (hasManyInfo.foreignKey || hasManyInfo.via)) {
1268
- let targetDescriptor;
1268
+ let targetDescriptor
1269
1269
  try {
1270
- targetDescriptor = await getDescriptor(hasManyInfo.target);
1270
+ targetDescriptor = await getDescriptor(hasManyInfo.target)
1271
1271
  } catch (error) {
1272
- continue;
1272
+ continue
1273
1273
  }
1274
1274
 
1275
1275
  const parentIds = resources
1276
1276
  .map((resource) => normalizeId(resource.id))
1277
- .filter((id) => id !== null);
1277
+ .filter((id) => id !== null)
1278
1278
 
1279
- if (parentIds.length === 0) continue;
1279
+ if (parentIds.length === 0) continue
1280
1280
 
1281
1281
  const queryIds = Array.from(new Set([
1282
1282
  ...parentIds,
1283
1283
  ...parentIds
1284
1284
  .map((id) => {
1285
- const numeric = Number(id);
1286
- return Number.isFinite(numeric) ? numeric : null;
1285
+ const numeric = Number(id)
1286
+ return Number.isFinite(numeric) ? numeric : null
1287
1287
  })
1288
1288
  .filter((value) => value !== null),
1289
- ]));
1289
+ ]))
1290
1290
 
1291
- let rows = [];
1292
- let groupingColumn = null;
1291
+ let rows = []
1292
+ let groupingColumn = null
1293
1293
 
1294
1294
  if (hasManyInfo.foreignKey) {
1295
- const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey];
1295
+ const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey]
1296
1296
  if (!foreignField?.slot) {
1297
- continue;
1297
+ continue
1298
1298
  }
1299
- groupingColumn = foreignField.slot;
1299
+ groupingColumn = foreignField.slot
1300
1300
  rows = await db(targetDescriptor.canonical.tableName)
1301
1301
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1302
1302
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1303
- .whereIn(groupingColumn, parentIds);
1303
+ .whereIn(groupingColumn, parentIds)
1304
1304
  } else if (hasManyInfo.via) {
1305
- const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via];
1305
+ const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via]
1306
1306
  if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
1307
- continue;
1307
+ continue
1308
1308
  }
1309
- groupingColumn = polyInfo.idColumn;
1309
+ groupingColumn = polyInfo.idColumn
1310
1310
  rows = await db(targetDescriptor.canonical.tableName)
1311
1311
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1312
1312
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1313
1313
  .where(polyInfo.typeColumn, descriptor.resource)
1314
- .whereIn(groupingColumn, queryIds);
1314
+ .whereIn(groupingColumn, queryIds)
1315
1315
  }
1316
1316
 
1317
1317
  if (!groupingColumn || rows.length === 0) {
1318
- continue;
1318
+ continue
1319
1319
  }
1320
1320
 
1321
- const newResources = [];
1322
- for (const row of rows) {
1323
- const includeKey = `${targetDescriptor.resource}:${row.id}`;
1324
- if (seen.has(includeKey)) continue;
1325
- seen.add(includeKey);
1326
- const translated = translateRecordFromStorage(row, targetDescriptor);
1327
- const includeResource = {
1328
- type: targetDescriptor.resource,
1329
- id: String(row.id),
1330
- attributes: translated.attributes,
1331
- };
1332
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1333
- includeResource.relationships = translated.relationships;
1334
- }
1335
- addSelfLink(includeResource, targetDescriptor.resource);
1336
- includes.push(includeResource);
1337
- newResources.push(includeResource);
1321
+ const newResources = []
1322
+ for (const row of rows) {
1323
+ const includeKey = `${targetDescriptor.resource}:${row.id}`
1324
+ if (seen.has(includeKey)) continue
1325
+ seen.add(includeKey)
1326
+ const translated = translateRecordFromStorage(row, targetDescriptor)
1327
+ const includeResource = {
1328
+ type: targetDescriptor.resource,
1329
+ id: String(row.id),
1330
+ attributes: translated.attributes,
1331
+ }
1332
+ if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1333
+ includeResource.relationships = translated.relationships
1338
1334
  }
1335
+ addSelfLink(includeResource, targetDescriptor.resource)
1336
+ includes.push(includeResource)
1337
+ newResources.push(includeResource)
1338
+ }
1339
1339
 
1340
1340
  if (childKeys.length > 0 && newResources.length > 0) {
1341
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1342
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1341
+ await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1342
+ await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1343
1343
  await collectIncludes({
1344
1344
  descriptor: targetDescriptor,
1345
1345
  resources: newResources,
@@ -1347,56 +1347,56 @@ export const RestApiAnyapiKnexPlugin = {
1347
1347
  context,
1348
1348
  includes,
1349
1349
  seen,
1350
- });
1350
+ })
1351
1351
  }
1352
1352
 
1353
- continue;
1353
+ continue
1354
1354
  }
1355
1355
 
1356
- const manyInfo = descriptor.manyToMany?.[relName];
1356
+ const manyInfo = descriptor.manyToMany?.[relName]
1357
1357
  if (manyInfo) {
1358
- const info = await getManyToManyInfo(descriptor.resource, relName);
1359
- if (!info) continue;
1358
+ const info = await getManyToManyInfo(descriptor.resource, relName)
1359
+ if (!info) continue
1360
1360
  const linkRows = await fetchLinksForParents({
1361
1361
  descriptor: info.descriptor,
1362
1362
  relationshipKey: info.relationshipKey,
1363
1363
  parentIds: resources.map((resource) => resource.id),
1364
1364
  db,
1365
- });
1365
+ })
1366
1366
 
1367
- const childIds = [...new Set(linkRows.map((row) => row.childId))];
1368
- if (childIds.length === 0) continue;
1367
+ const childIds = [...new Set(linkRows.map((row) => row.childId))]
1368
+ if (childIds.length === 0) continue
1369
1369
 
1370
- const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target);
1371
- if (!targetDescriptor) continue;
1370
+ const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target)
1371
+ if (!targetDescriptor) continue
1372
1372
 
1373
1373
  const rows = await db(targetDescriptor.canonical.tableName)
1374
1374
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1375
1375
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1376
- .whereIn('id', childIds);
1377
-
1378
- const newResources = [];
1379
- for (const row of rows) {
1380
- const includeKey = `${targetDescriptor.resource}:${row.id}`;
1381
- if (seen.has(includeKey)) continue;
1382
- seen.add(includeKey);
1383
- const translated = translateRecordFromStorage(row, targetDescriptor);
1384
- const includeResource = {
1385
- type: targetDescriptor.resource,
1386
- id: String(row.id),
1387
- attributes: translated.attributes,
1388
- };
1389
- if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1390
- includeResource.relationships = translated.relationships;
1391
- }
1392
- addSelfLink(includeResource, targetDescriptor.resource);
1393
- includes.push(includeResource);
1394
- newResources.push(includeResource);
1395
- }
1376
+ .whereIn('id', childIds)
1377
+
1378
+ const newResources = []
1379
+ for (const row of rows) {
1380
+ const includeKey = `${targetDescriptor.resource}:${row.id}`
1381
+ if (seen.has(includeKey)) continue
1382
+ seen.add(includeKey)
1383
+ const translated = translateRecordFromStorage(row, targetDescriptor)
1384
+ const includeResource = {
1385
+ type: targetDescriptor.resource,
1386
+ id: String(row.id),
1387
+ attributes: translated.attributes,
1388
+ }
1389
+ if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1390
+ includeResource.relationships = translated.relationships
1391
+ }
1392
+ addSelfLink(includeResource, targetDescriptor.resource)
1393
+ includes.push(includeResource)
1394
+ newResources.push(includeResource)
1395
+ }
1396
1396
 
1397
1397
  if (childKeys.length > 0 && newResources.length > 0) {
1398
- await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1399
- await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context });
1398
+ await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1399
+ await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
1400
1400
  await collectIncludes({
1401
1401
  descriptor: targetDescriptor,
1402
1402
  resources: newResources,
@@ -1404,18 +1404,18 @@ export const RestApiAnyapiKnexPlugin = {
1404
1404
  context,
1405
1405
  includes,
1406
1406
  seen,
1407
- });
1407
+ })
1408
1408
  }
1409
1409
  }
1410
1410
  }
1411
- };
1411
+ }
1412
1412
 
1413
1413
  const buildIncludes = async ({ parentResources, descriptor, context }) => {
1414
- const includeTree = parseIncludeTree(context.queryParams?.include);
1415
- if (Object.keys(includeTree).length === 0) return [];
1414
+ const includeTree = parseIncludeTree(context.queryParams?.include)
1415
+ if (Object.keys(includeTree).length === 0) return []
1416
1416
 
1417
- const includes = [];
1418
- const seen = new Set();
1417
+ const includes = []
1418
+ const seen = new Set()
1419
1419
 
1420
1420
  await collectIncludes({
1421
1421
  descriptor,
@@ -1424,221 +1424,221 @@ export const RestApiAnyapiKnexPlugin = {
1424
1424
  context,
1425
1425
  includes,
1426
1426
  seen,
1427
- });
1427
+ })
1428
1428
 
1429
- return includes;
1430
- };
1429
+ return includes
1430
+ }
1431
1431
 
1432
1432
  const attachHasManyRelationships = async ({ resources, descriptor, context }) => {
1433
- if (!resources || resources.length === 0) return;
1433
+ if (!resources || resources.length === 0) return
1434
1434
  const hasManyEntries = Object.entries(descriptor.relationships || {})
1435
- .filter(([, relDef]) => relDef?.type === 'hasMany' && relDef.target && (relDef.foreignKey || relDef.via));
1435
+ .filter(([, relDef]) => relDef?.type === 'hasMany' && relDef.target && (relDef.foreignKey || relDef.via))
1436
1436
 
1437
- if (hasManyEntries.length === 0) return;
1437
+ if (hasManyEntries.length === 0) return
1438
1438
 
1439
- const db = context.db || context.transaction || api.knex.instance;
1440
- const parentIds = resources.map((resource) => resource.id);
1439
+ const db = context.db || context.transaction || api.knex.instance
1440
+ const parentIds = resources.map((resource) => resource.id)
1441
1441
  const normalizedParentIds = parentIds
1442
1442
  .map((id) => normalizeId(id))
1443
- .filter((id) => id !== null);
1443
+ .filter((id) => id !== null)
1444
1444
 
1445
- if (normalizedParentIds.length === 0) return;
1445
+ if (normalizedParentIds.length === 0) return
1446
1446
 
1447
1447
  for (const [relName, relDef] of hasManyEntries) {
1448
- let targetDescriptor;
1448
+ let targetDescriptor
1449
1449
  try {
1450
- targetDescriptor = await getDescriptor(relDef.target);
1450
+ targetDescriptor = await getDescriptor(relDef.target)
1451
1451
  } catch (error) {
1452
- continue;
1452
+ continue
1453
1453
  }
1454
1454
 
1455
1455
  if (relDef.foreignKey) {
1456
- const foreignField = targetDescriptor.fields?.[relDef.foreignKey];
1457
- if (!foreignField?.slot) continue;
1458
- const column = foreignField.slot;
1456
+ const foreignField = targetDescriptor.fields?.[relDef.foreignKey]
1457
+ if (!foreignField?.slot) continue
1458
+ const column = foreignField.slot
1459
1459
 
1460
1460
  const rows = await db(targetDescriptor.canonical.tableName)
1461
1461
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1462
1462
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1463
- .whereIn(column, normalizedParentIds);
1463
+ .whereIn(column, normalizedParentIds)
1464
1464
 
1465
1465
  const grouped = rows.reduce((acc, row) => {
1466
- const parentId = row[column];
1467
- if (parentId == null) return acc;
1468
- const key = String(parentId);
1469
- acc[key] = acc[key] || [];
1470
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) });
1471
- return acc;
1472
- }, {});
1466
+ const parentId = row[column]
1467
+ if (parentId == null) return acc
1468
+ const key = String(parentId)
1469
+ acc[key] = acc[key] || []
1470
+ acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1471
+ return acc
1472
+ }, {})
1473
1473
 
1474
1474
  for (const resource of resources) {
1475
- resource.relationships = resource.relationships || {};
1476
- const related = grouped[String(resource.id)] || [];
1477
- resource.relationships[relName] = { data: related };
1475
+ resource.relationships = resource.relationships || {}
1476
+ const related = grouped[String(resource.id)] || []
1477
+ resource.relationships[relName] = { data: related }
1478
1478
  }
1479
1479
 
1480
- continue;
1480
+ continue
1481
1481
  }
1482
1482
 
1483
1483
  if (relDef.via) {
1484
- const polyInfo = targetDescriptor.polymorphicBelongsTo?.[relDef.via];
1485
- if (!polyInfo?.idColumn || !polyInfo?.typeColumn) continue;
1484
+ const polyInfo = targetDescriptor.polymorphicBelongsTo?.[relDef.via]
1485
+ if (!polyInfo?.idColumn || !polyInfo?.typeColumn) continue
1486
1486
 
1487
1487
  const queryIds = Array.from(new Set([
1488
1488
  ...normalizedParentIds,
1489
1489
  ...normalizedParentIds
1490
1490
  .map((id) => {
1491
- const numeric = Number(id);
1492
- return Number.isFinite(numeric) ? numeric : null;
1491
+ const numeric = Number(id)
1492
+ return Number.isFinite(numeric) ? numeric : null
1493
1493
  })
1494
1494
  .filter((value) => value !== null),
1495
- ]));
1495
+ ]))
1496
1496
 
1497
1497
  const rows = await db(targetDescriptor.canonical.tableName)
1498
1498
  .where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
1499
1499
  .where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
1500
1500
  .where(polyInfo.typeColumn, descriptor.resource)
1501
- .whereIn(polyInfo.idColumn, queryIds);
1501
+ .whereIn(polyInfo.idColumn, queryIds)
1502
1502
 
1503
1503
  const grouped = rows.reduce((acc, row) => {
1504
- const parentId = row[polyInfo.idColumn];
1505
- if (parentId == null) return acc;
1506
- const key = String(parentId);
1507
- acc[key] = acc[key] || [];
1508
- acc[key].push({ type: targetDescriptor.resource, id: String(row.id) });
1509
- return acc;
1510
- }, {});
1504
+ const parentId = row[polyInfo.idColumn]
1505
+ if (parentId == null) return acc
1506
+ const key = String(parentId)
1507
+ acc[key] = acc[key] || []
1508
+ acc[key].push({ type: targetDescriptor.resource, id: String(row.id) })
1509
+ return acc
1510
+ }, {})
1511
1511
 
1512
1512
  for (const resource of resources) {
1513
- resource.relationships = resource.relationships || {};
1514
- const related = grouped[String(resource.id)] || [];
1515
- resource.relationships[relName] = { data: related };
1513
+ resource.relationships = resource.relationships || {}
1514
+ const related = grouped[String(resource.id)] || []
1515
+ resource.relationships[relName] = { data: related }
1516
1516
  }
1517
1517
  }
1518
1518
  }
1519
- };
1519
+ }
1520
1520
 
1521
1521
  const attachManyToManyRelationships = async ({ resources, descriptor, context }) => {
1522
- if (!resources || resources.length === 0) return;
1523
- const manyEntries = Object.entries(descriptor.manyToMany || {});
1524
- if (manyEntries.length === 0) return;
1522
+ if (!resources || resources.length === 0) return
1523
+ const manyEntries = Object.entries(descriptor.manyToMany || {})
1524
+ if (manyEntries.length === 0) return
1525
1525
 
1526
- const db = context.db || context.transaction || api.knex.instance;
1527
- const parentIds = resources.map((resource) => resource.id);
1526
+ const db = context.db || context.transaction || api.knex.instance
1527
+ const parentIds = resources.map((resource) => resource.id)
1528
1528
 
1529
1529
  for (const [relName, relInfo] of manyEntries) {
1530
- const info = await getManyToManyInfo(descriptor.resource, relName);
1531
- if (!info) continue;
1530
+ const info = await getManyToManyInfo(descriptor.resource, relName)
1531
+ if (!info) continue
1532
1532
  const rows = await fetchLinksForParents({
1533
1533
  descriptor: info.descriptor,
1534
1534
  relationshipKey: info.relationshipKey,
1535
1535
  parentIds,
1536
1536
  db,
1537
- });
1537
+ })
1538
1538
 
1539
1539
  const grouped = rows.reduce((acc, row) => {
1540
- const key = String(row.parentId);
1541
- acc[key] = acc[key] || [];
1542
- acc[key].push({ type: row.childType, id: String(row.childId) });
1543
- return acc;
1544
- }, {});
1540
+ const key = String(row.parentId)
1541
+ acc[key] = acc[key] || []
1542
+ acc[key].push({ type: row.childType, id: String(row.childId) })
1543
+ return acc
1544
+ }, {})
1545
1545
 
1546
1546
  for (const resource of resources) {
1547
- const related = grouped[resource.id] || [];
1548
- resource.relationships = resource.relationships || {};
1549
- resource.relationships[relName] = { data: related };
1547
+ const related = grouped[resource.id] || []
1548
+ resource.relationships = resource.relationships || {}
1549
+ resource.relationships[relName] = { data: related }
1550
1550
  }
1551
1551
  }
1552
- };
1552
+ }
1553
1553
 
1554
1554
  helpers.dataGet = async ({ scopeName, context }) => {
1555
- const storageAdapter = getScopeStorageAdapter(scopeName);
1555
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1556
1556
  if (storageAdapter) {
1557
- context.storageAdapter = storageAdapter;
1557
+ context.storageAdapter = storageAdapter
1558
1558
  }
1559
- const descriptor = await getDescriptor(scopeName);
1560
- const { canonical } = descriptor;
1561
- const id = context.id;
1562
- const scope = api.resources?.[scopeName];
1559
+ const descriptor = await getDescriptor(scopeName)
1560
+ const { canonical } = descriptor
1561
+ const id = context.id
1562
+ const scope = api.resources?.[scopeName]
1563
1563
 
1564
- let fieldSelectionInfo = null;
1564
+ let fieldSelectionInfo = null
1565
1565
  if (scope) {
1566
- fieldSelectionInfo = await buildFieldSelection(scope, { context });
1567
- context.computedDependencies = fieldSelectionInfo.computedDependencies;
1566
+ fieldSelectionInfo = await buildFieldSelection(scope, { context })
1567
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
1568
1568
  } else {
1569
- context.computedDependencies = [];
1569
+ context.computedDependencies = []
1570
1570
  }
1571
1571
 
1572
1572
  const row = await context.db(canonical.tableName)
1573
1573
  .where('id', id)
1574
1574
  .where(canonical.resourceColumn, descriptor.resource)
1575
1575
  .where(canonical.tenantColumn, descriptor.tenant)
1576
- .first();
1576
+ .first()
1577
1577
 
1578
- if (!row) return null;
1578
+ if (!row) return null
1579
1579
 
1580
- const record = translateRecordFromStorage(row, descriptor);
1580
+ const record = translateRecordFromStorage(row, descriptor)
1581
1581
  const data = {
1582
1582
  type: descriptor.resource,
1583
1583
  id: String(row.id),
1584
1584
  attributes: record.attributes,
1585
- };
1585
+ }
1586
1586
 
1587
1587
  if (record.relationships && Object.keys(record.relationships).length > 0) {
1588
- data.relationships = record.relationships;
1588
+ data.relationships = record.relationships
1589
1589
  }
1590
1590
 
1591
1591
  await attachHasManyRelationships({
1592
1592
  resources: [data],
1593
1593
  descriptor,
1594
1594
  context,
1595
- });
1595
+ })
1596
1596
 
1597
1597
  await attachManyToManyRelationships({
1598
1598
  resources: [data],
1599
1599
  descriptor,
1600
1600
  context,
1601
- });
1601
+ })
1602
1602
 
1603
1603
  const included = await buildIncludes({
1604
1604
  parentResources: [data],
1605
1605
  descriptor,
1606
1606
  context,
1607
- });
1607
+ })
1608
1608
 
1609
- const response = { data };
1609
+ const response = { data }
1610
1610
  if (included.length > 0) {
1611
- response.included = included;
1611
+ response.included = included
1612
1612
  }
1613
1613
 
1614
1614
  if (scope) {
1615
- data.links = data.links || {};
1616
- data.links.self = buildResourceUrl(context, scope, scopeName, data.id);
1615
+ data.links = data.links || {}
1616
+ data.links.self = buildResourceUrl(context, scope, scopeName, data.id)
1617
1617
  response.links = {
1618
1618
  self: data.links.self,
1619
- };
1619
+ }
1620
1620
  }
1621
1621
 
1622
1622
  if (included.length > 0) {
1623
1623
  for (const includeResource of included) {
1624
- const targetScope = api.resources?.[includeResource.type];
1625
- if (!targetScope) continue;
1626
- includeResource.links = includeResource.links || {};
1624
+ const targetScope = api.resources?.[includeResource.type]
1625
+ if (!targetScope) continue
1626
+ includeResource.links = includeResource.links || {}
1627
1627
  includeResource.links.self = buildResourceUrl(
1628
1628
  context,
1629
1629
  targetScope,
1630
1630
  includeResource.type,
1631
- includeResource.id,
1632
- );
1631
+ includeResource.id
1632
+ )
1633
1633
  }
1634
1634
  }
1635
1635
 
1636
- return response;
1637
- };
1636
+ return response
1637
+ }
1638
1638
 
1639
1639
  const rehydrateSchemaInfo = async (scopeName) => {
1640
- const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {};
1641
- const storedOptions = scopeOptionsRegistry.get(scopeName) || {};
1640
+ const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {}
1641
+ const storedOptions = scopeOptionsRegistry.get(scopeName) || {}
1642
1642
 
1643
1643
  if (!scope.scopeOptions) {
1644
1644
  scope.scopeOptions = {
@@ -1648,56 +1648,56 @@ export const RestApiAnyapiKnexPlugin = {
1648
1648
  tableName: storedOptions.tableName || scopeName,
1649
1649
  idProperty: storedOptions.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
1650
1650
  canonicalFieldsMap: storedOptions.canonicalFieldsMap || {},
1651
- };
1651
+ }
1652
1652
  }
1653
1653
 
1654
- const descriptor = await registry.getDescriptor(tenantId, scopeName);
1655
- if (!descriptor) return;
1654
+ const descriptor = await registry.getDescriptor(tenantId, scopeName)
1655
+ if (!descriptor) return
1656
1656
 
1657
- const existingInfo = scope.vars?.schemaInfo || {};
1657
+ const existingInfo = scope.vars?.schemaInfo || {}
1658
1658
 
1659
- const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) };
1660
- const computed = { ...(existingInfo.computed || {}) };
1659
+ const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) }
1660
+ const computed = { ...(existingInfo.computed || {}) }
1661
1661
 
1662
1662
  for (const [fieldName, definition] of Object.entries(descriptor.schema || {})) {
1663
1663
  if (definition?.computed) {
1664
1664
  if (!computed[fieldName]) {
1665
- computed[fieldName] = { ...definition };
1665
+ computed[fieldName] = { ...definition }
1666
1666
  }
1667
- continue;
1667
+ continue
1668
1668
  }
1669
1669
  mergedSchema[fieldName] = {
1670
1670
  ...(mergedSchema[fieldName] || {}),
1671
1671
  ...definition,
1672
- };
1672
+ }
1673
1673
  }
1674
1674
 
1675
1675
  if (!mergedSchema.id) {
1676
- mergedSchema.id = { type: 'id' };
1676
+ mergedSchema.id = { type: 'id' }
1677
1677
  }
1678
1678
 
1679
- const schemaInstance = createSchema(mergedSchema);
1679
+ const schemaInstance = createSchema(mergedSchema)
1680
1680
 
1681
1681
  const rawSearchFields = generateSearchSchemaFromSchema(
1682
1682
  mergedSchema,
1683
- scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null,
1684
- );
1683
+ scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null
1684
+ )
1685
1685
  if (rawSearchFields) {
1686
- ensureSearchFieldsAreIndexed(rawSearchFields);
1686
+ ensureSearchFieldsAreIndexed(rawSearchFields)
1687
1687
  }
1688
- const searchSchemaInstance = createSchema(rawSearchFields || {});
1688
+ const searchSchemaInstance = createSchema(rawSearchFields || {})
1689
1689
 
1690
- const tableName = storedOptions.tableName
1691
- || existingInfo.tableName
1692
- || scope.scopeOptions?.tableName
1693
- || scopeName;
1690
+ const tableName = storedOptions.tableName ||
1691
+ existingInfo.tableName ||
1692
+ scope.scopeOptions?.tableName ||
1693
+ scopeName
1694
1694
 
1695
- const idProperty = storedOptions.idProperty
1696
- || existingInfo.idProperty
1697
- || scope.scopeOptions?.idProperty
1698
- || scope.vars?.idProperty
1699
- || vars.idProperty
1700
- || 'id';
1695
+ const idProperty = storedOptions.idProperty ||
1696
+ existingInfo.idProperty ||
1697
+ scope.scopeOptions?.idProperty ||
1698
+ scope.vars?.idProperty ||
1699
+ vars.idProperty ||
1700
+ 'id'
1701
1701
 
1702
1702
  scope.vars.schemaInfo = {
1703
1703
  ...existingInfo,
@@ -1705,24 +1705,24 @@ export const RestApiAnyapiKnexPlugin = {
1705
1705
  schemaStructure: schemaInstance.structure,
1706
1706
  searchSchemaInstance,
1707
1707
  searchSchemaStructure: searchSchemaInstance.structure,
1708
- schemaRelationships: descriptor.relationships
1709
- || scope.scopeOptions?.relationships
1710
- || storedOptions.relationships
1711
- || existingInfo.schemaRelationships
1712
- || {},
1708
+ schemaRelationships: descriptor.relationships ||
1709
+ scope.scopeOptions?.relationships ||
1710
+ storedOptions.relationships ||
1711
+ existingInfo.schemaRelationships ||
1712
+ {},
1713
1713
  computed,
1714
1714
  tableName,
1715
1715
  idProperty,
1716
1716
  descriptor,
1717
1717
  canonicalFieldMap: descriptor.canonicalFieldMap || {},
1718
- };
1718
+ }
1719
1719
 
1720
- getScopeStorageAdapter(scopeName);
1720
+ getScopeStorageAdapter(scopeName)
1721
1721
 
1722
1722
  scope.scopeOptions = {
1723
1723
  ...(scope.scopeOptions || {}),
1724
1724
  canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
1725
- };
1725
+ }
1726
1726
 
1727
1727
  scopeOptionsRegistry.set(scopeName, {
1728
1728
  schema: storedOptions.schema || scope.scopeOptions?.schema || {},
@@ -1731,89 +1731,89 @@ export const RestApiAnyapiKnexPlugin = {
1731
1731
  tableName,
1732
1732
  idProperty,
1733
1733
  canonicalFieldsMap: descriptor.canonicalFieldMap || {},
1734
- });
1735
- };
1734
+ })
1735
+ }
1736
1736
 
1737
1737
  const buildTableNameMaps = () => {
1738
- const resourceToTableName = new Map();
1739
- const tableNameToResource = new Map();
1738
+ const resourceToTableName = new Map()
1739
+ const tableNameToResource = new Map()
1740
1740
  for (const [resourceName, resourceScope] of Object.entries(api.resources || {})) {
1741
- const tableName = resourceScope?.vars?.schemaInfo?.tableName;
1742
- if (!tableName) continue;
1743
- resourceToTableName.set(resourceName, tableName);
1744
- tableNameToResource.set(tableName, resourceName);
1741
+ const tableName = resourceScope?.vars?.schemaInfo?.tableName
1742
+ if (!tableName) continue
1743
+ resourceToTableName.set(resourceName, tableName)
1744
+ tableNameToResource.set(tableName, resourceName)
1745
1745
  }
1746
- return { resourceToTableName, tableNameToResource };
1747
- };
1746
+ return { resourceToTableName, tableNameToResource }
1747
+ }
1748
1748
 
1749
1749
  const translateSelectFieldsForAdapter = (fields, adapter) => {
1750
- if (!adapter || !fields) return fields;
1750
+ if (!adapter || !fields) return fields
1751
1751
 
1752
1752
  return fields.map((field) => {
1753
- if (typeof field !== 'string') return field;
1753
+ if (typeof field !== 'string') return field
1754
1754
 
1755
- if (field === '*') return '*';
1755
+ if (field === '*') return '*'
1756
1756
 
1757
- const aliasMatch = field.match(/\s+as\s+/i);
1757
+ const aliasMatch = field.match(/\s+as\s+/i)
1758
1758
  if (aliasMatch) {
1759
- const [source, alias] = field.split(/\s+as\s+/i);
1760
- const translatedSource = adapter.translateColumn(source.trim()) || source.trim();
1761
- return `${translatedSource} as ${alias.trim()}`;
1759
+ const [source, alias] = field.split(/\s+as\s+/i)
1760
+ const translatedSource = adapter.translateColumn(source.trim()) || source.trim()
1761
+ return `${translatedSource} as ${alias.trim()}`
1762
1762
  }
1763
1763
 
1764
- const translated = adapter.translateColumn(field);
1765
- return translated || field;
1766
- });
1767
- };
1764
+ const translated = adapter.translateColumn(field)
1765
+ return translated || field
1766
+ })
1767
+ }
1768
1768
 
1769
1769
  const scopesProxy = new Proxy({}, {
1770
- get(_target, prop) {
1770
+ get (_target, prop) {
1771
1771
  if (typeof prop !== 'string') {
1772
- return undefined;
1772
+ return undefined
1773
1773
  }
1774
- const resource = api.resources?.[prop];
1774
+ const resource = api.resources?.[prop]
1775
1775
  if (resource?.vars?.schemaInfo) {
1776
- return resource;
1776
+ return resource
1777
1777
  }
1778
- return scopes?.[prop];
1778
+ return scopes?.[prop]
1779
1779
  },
1780
- });
1780
+ })
1781
1781
 
1782
- const queryHookDependencies = { log, scopes: scopesProxy, knex, getStorageAdapter: getScopeStorageAdapter };
1782
+ const queryHookDependencies = { log, scopes: scopesProxy, knex, getStorageAdapter: getScopeStorageAdapter }
1783
1783
 
1784
1784
  addHook('knexQueryFiltering', 'polymorphicFiltersHook', {}, async (hookParams) =>
1785
1785
  polymorphicFiltersHook(hookParams, queryHookDependencies)
1786
- );
1786
+ )
1787
1787
 
1788
1788
  addHook('knexQueryFiltering', 'crossTableFiltersHook', {}, async (hookParams) =>
1789
1789
  crossTableFiltersHook(hookParams, queryHookDependencies)
1790
- );
1790
+ )
1791
1791
 
1792
1792
  addHook('knexQueryFiltering', 'basicFiltersHook', {}, async (hookParams) =>
1793
1793
  basicFiltersHook(hookParams, queryHookDependencies)
1794
- );
1794
+ )
1795
1795
 
1796
1796
  helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
1797
- const storageAdapter = getScopeStorageAdapter(scopeName);
1797
+ const storageAdapter = getScopeStorageAdapter(scopeName)
1798
1798
  if (storageAdapter) {
1799
- context.storageAdapter = storageAdapter;
1799
+ context.storageAdapter = storageAdapter
1800
1800
  }
1801
- const descriptor = await getDescriptor(scopeName);
1802
- const db = context.db || context.transaction || api.knex.instance;
1803
- const scope = api.resources?.[scopeName];
1804
- const queryParams = context.queryParams || {};
1801
+ const descriptor = await getDescriptor(scopeName)
1802
+ const db = context.db || context.transaction || api.knex.instance
1803
+ const scope = api.resources?.[scopeName]
1804
+ const queryParams = context.queryParams || {}
1805
1805
 
1806
- let fieldSelectionInfo = null;
1806
+ let fieldSelectionInfo = null
1807
1807
  if (scope) {
1808
- fieldSelectionInfo = await buildFieldSelection(scope, { context });
1809
- context.computedDependencies = fieldSelectionInfo.computedDependencies;
1808
+ fieldSelectionInfo = await buildFieldSelection(scope, { context })
1809
+ context.computedDependencies = fieldSelectionInfo.computedDependencies
1810
1810
  } else {
1811
- context.computedDependencies = [];
1811
+ context.computedDependencies = []
1812
1812
  }
1813
1813
 
1814
- const { resourceToTableName, tableNameToResource } = buildTableNameMaps();
1815
- context.resourceToTableName = resourceToTableName;
1816
- const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor });
1814
+ const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
1815
+ context.resourceToTableName = resourceToTableName
1816
+ const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
1817
1817
  const adapter = new AnyapiQueryAdapter({
1818
1818
  descriptor,
1819
1819
  db,
@@ -1822,11 +1822,11 @@ export const RestApiAnyapiKnexPlugin = {
1822
1822
  resourceToTableName,
1823
1823
  tableNameToResource,
1824
1824
  log,
1825
- });
1826
- const queryBuilder = adapter.query;
1825
+ })
1826
+ const queryBuilder = adapter.query
1827
1827
 
1828
- const schemaInfo = scope?.vars?.schemaInfo;
1829
- const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias;
1828
+ const schemaInfo = scope?.vars?.schemaInfo
1829
+ const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
1830
1830
 
1831
1831
  applyFiltersToQuery({
1832
1832
  query: queryBuilder,
@@ -1834,7 +1834,7 @@ export const RestApiAnyapiKnexPlugin = {
1834
1834
  descriptor,
1835
1835
  searchSchema: schemaInfo?.searchSchemaStructure,
1836
1836
  adapter,
1837
- });
1837
+ })
1838
1838
 
1839
1839
  context.knexQuery = {
1840
1840
  query: queryBuilder,
@@ -1846,25 +1846,25 @@ export const RestApiAnyapiKnexPlugin = {
1846
1846
  isAnyApi: true,
1847
1847
  adapter,
1848
1848
  storageAdapter,
1849
- };
1849
+ }
1850
1850
 
1851
1851
  if (runHooks) {
1852
- await runHooks('knexQueryFiltering');
1852
+ await runHooks('knexQueryFiltering')
1853
1853
  }
1854
1854
 
1855
- delete context.knexQuery;
1855
+ delete context.knexQuery
1856
1856
 
1857
- const countQuery = queryBuilder.clone();
1857
+ const countQuery = queryBuilder.clone()
1858
1858
 
1859
1859
  const sortDescriptors = applySortingToQuery({
1860
1860
  query: queryBuilder,
1861
1861
  sort: queryParams.sort,
1862
1862
  descriptor,
1863
1863
  scope,
1864
- });
1864
+ })
1865
1865
 
1866
1866
  if (queryParams?.page?.after) {
1867
- }
1867
+ }
1868
1868
 
1869
1869
  const paginationInfo = applyPaginationToQuery({
1870
1870
  query: queryBuilder,
@@ -1872,120 +1872,120 @@ export const RestApiAnyapiKnexPlugin = {
1872
1872
  queryParams,
1873
1873
  sortDescriptors,
1874
1874
  adapter,
1875
- });
1875
+ })
1876
1876
 
1877
1877
  if (queryParams?.page?.after) {
1878
- }
1878
+ }
1879
1879
 
1880
1880
  if (fieldSelectionInfo && fieldSelectionInfo.fieldsToSelect && fieldSelectionInfo.fieldsToSelect.length > 0) {
1881
- const translatedSelect = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, adapter);
1882
- queryBuilder.select(translatedSelect);
1881
+ const translatedSelect = translateSelectFieldsForAdapter(fieldSelectionInfo.fieldsToSelect, adapter)
1882
+ queryBuilder.select(translatedSelect)
1883
1883
  }
1884
1884
 
1885
- let rows = await queryBuilder;
1886
- let cursorRecords = null;
1887
- let hasMore = false;
1885
+ let rows = await queryBuilder
1886
+ let cursorRecords = null
1887
+ let hasMore = false
1888
1888
 
1889
1889
  if (paginationInfo.mode === 'cursor') {
1890
- const { pageSize, sortDescriptors: cursorDescriptors } = paginationInfo;
1890
+ const { pageSize, sortDescriptors: cursorDescriptors } = paginationInfo
1891
1891
  cursorRecords = rows.map((row) => {
1892
- const record = {};
1892
+ const record = {}
1893
1893
  for (const descriptorEntry of cursorDescriptors) {
1894
1894
  if (descriptorEntry.field === 'id') {
1895
- record.id = row.id;
1895
+ record.id = row.id
1896
1896
  } else if (descriptorEntry.column) {
1897
- record[descriptorEntry.field] = row[descriptorEntry.column];
1897
+ record[descriptorEntry.field] = row[descriptorEntry.column]
1898
1898
  }
1899
1899
  }
1900
- return record;
1901
- });
1900
+ return record
1901
+ })
1902
1902
 
1903
1903
  if (rows.length > pageSize) {
1904
- hasMore = true;
1905
- rows = rows.slice(0, pageSize);
1906
- cursorRecords = cursorRecords.slice(0, pageSize);
1904
+ hasMore = true
1905
+ rows = rows.slice(0, pageSize)
1906
+ cursorRecords = cursorRecords.slice(0, pageSize)
1907
1907
  }
1908
1908
  }
1909
1909
 
1910
1910
  const data = rows.map((row) => {
1911
- const translated = translateRecordFromStorage(row, descriptor);
1911
+ const translated = translateRecordFromStorage(row, descriptor)
1912
1912
  const resource = {
1913
1913
  type: descriptor.resource,
1914
1914
  id: String(row.id),
1915
1915
  attributes: translated.attributes,
1916
- };
1916
+ }
1917
1917
  if (translated.relationships && Object.keys(translated.relationships).length > 0) {
1918
- resource.relationships = translated.relationships;
1918
+ resource.relationships = translated.relationships
1919
1919
  }
1920
- const resourceScope = scope;
1920
+ const resourceScope = scope
1921
1921
  if (resourceScope) {
1922
- resource.links = resource.links || {};
1923
- resource.links.self = buildResourceUrl(context, resourceScope, scopeName, resource.id);
1922
+ resource.links = resource.links || {}
1923
+ resource.links.self = buildResourceUrl(context, resourceScope, scopeName, resource.id)
1924
1924
  }
1925
1925
  if (fieldSelectionInfo?.computedDependencies?.length) {
1926
- resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies;
1926
+ resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
1927
1927
  }
1928
- return resource;
1929
- });
1928
+ return resource
1929
+ })
1930
1930
 
1931
1931
  await attachHasManyRelationships({
1932
1932
  resources: data,
1933
1933
  descriptor,
1934
1934
  context,
1935
- });
1935
+ })
1936
1936
 
1937
1937
  await attachManyToManyRelationships({
1938
1938
  resources: data,
1939
1939
  descriptor,
1940
1940
  context,
1941
- });
1941
+ })
1942
1942
 
1943
1943
  const included = await buildIncludes({
1944
1944
  parentResources: data,
1945
1945
  descriptor,
1946
1946
  context,
1947
- });
1947
+ })
1948
1948
 
1949
- const response = { data };
1949
+ const response = { data }
1950
1950
  if (included.length > 0) {
1951
- response.included = included;
1951
+ response.included = included
1952
1952
  }
1953
1953
 
1954
- context.returnMeta = context.returnMeta || {};
1955
- context.returnMeta.queryString = buildQueryString(queryParams);
1956
- delete context.returnMeta.paginationMeta;
1957
- delete context.returnMeta.paginationLinks;
1954
+ context.returnMeta = context.returnMeta || {}
1955
+ context.returnMeta.queryString = buildQueryString(queryParams)
1956
+ delete context.returnMeta.paginationMeta
1957
+ delete context.returnMeta.paginationLinks
1958
1958
 
1959
1959
  if (paginationInfo.mode === 'offset') {
1960
- const { page, pageSize } = paginationInfo;
1961
- let paginationMeta;
1960
+ const { page, pageSize } = paginationInfo
1961
+ let paginationMeta
1962
1962
 
1963
1963
  if (scope?.vars?.enablePaginationCounts) {
1964
- const countResult = await countQuery.count({ count: '*' }).first();
1965
- const total = Number(countResult?.count ?? countResult?.total ?? 0);
1966
- paginationMeta = calculatePaginationMeta(total, page, pageSize);
1964
+ const countResult = await countQuery.count({ count: '*' }).first()
1965
+ const total = Number(countResult?.count ?? countResult?.total ?? 0)
1966
+ paginationMeta = calculatePaginationMeta(total, page, pageSize)
1967
1967
  } else {
1968
- paginationMeta = { page, pageSize };
1968
+ paginationMeta = { page, pageSize }
1969
1969
  }
1970
1970
 
1971
- context.returnMeta.paginationMeta = paginationMeta;
1972
- const urlPrefix = getUrlPrefix(context, scope);
1971
+ context.returnMeta.paginationMeta = paginationMeta
1972
+ const urlPrefix = getUrlPrefix(context, scope)
1973
1973
  context.returnMeta.paginationLinks = generatePaginationLinks(
1974
1974
  urlPrefix,
1975
1975
  scopeName,
1976
1976
  queryParams,
1977
- paginationMeta,
1978
- );
1977
+ paginationMeta
1978
+ )
1979
1979
  } else if (paginationInfo.mode === 'cursor' && cursorRecords) {
1980
- const cursorFields = paginationInfo.sortDescriptors.map((descriptorEntry) => descriptorEntry.field);
1980
+ const cursorFields = paginationInfo.sortDescriptors.map((descriptorEntry) => descriptorEntry.field)
1981
1981
  const paginationMeta = buildCursorMeta(
1982
1982
  cursorRecords,
1983
1983
  paginationInfo.pageSize,
1984
1984
  hasMore,
1985
- cursorFields,
1986
- );
1987
- context.returnMeta.paginationMeta = paginationMeta;
1988
- const urlPrefix = getUrlPrefix(context, scope);
1985
+ cursorFields
1986
+ )
1987
+ context.returnMeta.paginationMeta = paginationMeta
1988
+ const urlPrefix = getUrlPrefix(context, scope)
1989
1989
  context.returnMeta.paginationLinks = generateCursorPaginationLinks(
1990
1990
  urlPrefix,
1991
1991
  scopeName,
@@ -1993,37 +1993,37 @@ export const RestApiAnyapiKnexPlugin = {
1993
1993
  cursorRecords,
1994
1994
  paginationInfo.pageSize,
1995
1995
  hasMore,
1996
- cursorFields,
1997
- );
1996
+ cursorFields
1997
+ )
1998
1998
  }
1999
1999
 
2000
2000
  if (context.returnMeta.paginationMeta) {
2001
2001
  response.meta = {
2002
2002
  pagination: context.returnMeta.paginationMeta,
2003
- };
2003
+ }
2004
2004
  }
2005
2005
 
2006
2006
  if (context.returnMeta.paginationLinks) {
2007
- response.links = context.returnMeta.paginationLinks;
2007
+ response.links = context.returnMeta.paginationLinks
2008
2008
  } else if (scope) {
2009
- const urlPrefix = getUrlPrefix(context, scope);
2009
+ const urlPrefix = getUrlPrefix(context, scope)
2010
2010
  response.links = {
2011
2011
  self: `${urlPrefix}/${scopeName}${context.returnMeta.queryString || ''}`,
2012
- };
2012
+ }
2013
2013
  }
2014
2014
 
2015
- return response;
2016
- };
2015
+ return response
2016
+ }
2017
2017
 
2018
2018
  helpers.dataQueryCount = async ({ scopeName, context }) => {
2019
- const storageAdapter = getScopeStorageAdapter(scopeName);
2019
+ const storageAdapter = getScopeStorageAdapter(scopeName)
2020
2020
  if (storageAdapter) {
2021
- context.storageAdapter = storageAdapter;
2021
+ context.storageAdapter = storageAdapter
2022
2022
  }
2023
- const descriptor = await getDescriptor(scopeName);
2024
- const db = context.db || context.transaction || api.knex.instance;
2025
- const { resourceToTableName, tableNameToResource } = buildTableNameMaps();
2026
- const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor });
2023
+ const descriptor = await getDescriptor(scopeName)
2024
+ const db = context.db || context.transaction || api.knex.instance
2025
+ const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
2026
+ const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
2027
2027
  const adapter = new AnyapiQueryAdapter({
2028
2028
  descriptor,
2029
2029
  db,
@@ -2032,33 +2032,33 @@ export const RestApiAnyapiKnexPlugin = {
2032
2032
  resourceToTableName,
2033
2033
  tableNameToResource,
2034
2034
  log,
2035
- });
2035
+ })
2036
2036
 
2037
2037
  applyFiltersToQuery({
2038
2038
  query: adapter.query,
2039
2039
  filters: context.queryParams?.filters,
2040
2040
  descriptor,
2041
- });
2041
+ })
2042
2042
 
2043
- const [{ count }] = await adapter.query.count({ count: '*' });
2044
- return Number(count);
2045
- };
2043
+ const [{ count }] = await adapter.query.count({ count: '*' })
2044
+ return Number(count)
2045
+ }
2046
2046
 
2047
2047
  addHook('scope:added', 'anyapi-register-resource', { sequence: 50 }, async ({ context }) => {
2048
- const { scopeName, scopeOptions = {} } = context;
2049
- const scope = api.scopes?.[scopeName] || scopes?.[scopeName];
2050
- const stored = scopeOptionsRegistry.get(scopeName) || {};
2051
-
2052
- const baseSchema = scopeOptions.schema || scope?.scopeOptions?.schema || stored.schema || {};
2053
- const relationships = scopeOptions.relationships || scope?.scopeOptions?.relationships || stored.relationships || {};
2054
- const searchSchemaDef = scopeOptions.searchSchema || scope?.scopeOptions?.searchSchema || stored.searchSchema || null;
2055
- const tableName = scopeOptions.tableName || scope?.scopeOptions?.tableName || stored.tableName || scopeName;
2056
- const idProperty = scopeOptions.idProperty || scope?.scopeOptions?.idProperty || stored.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id';
2057
- const canonicalFieldsMap = scope?.scopeOptions?.canonicalFieldsMap || stored.canonicalFieldsMap || null;
2058
-
2059
- const schema = { ...baseSchema };
2048
+ const { scopeName, scopeOptions = {} } = context
2049
+ const scope = api.scopes?.[scopeName] || scopes?.[scopeName]
2050
+ const stored = scopeOptionsRegistry.get(scopeName) || {}
2051
+
2052
+ const baseSchema = scopeOptions.schema || scope?.scopeOptions?.schema || stored.schema || {}
2053
+ const relationships = scopeOptions.relationships || scope?.scopeOptions?.relationships || stored.relationships || {}
2054
+ const searchSchemaDef = scopeOptions.searchSchema || scope?.scopeOptions?.searchSchema || stored.searchSchema || null
2055
+ const tableName = scopeOptions.tableName || scope?.scopeOptions?.tableName || stored.tableName || scopeName
2056
+ const idProperty = scopeOptions.idProperty || scope?.scopeOptions?.idProperty || stored.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id'
2057
+ const canonicalFieldsMap = scope?.scopeOptions?.canonicalFieldsMap || stored.canonicalFieldsMap || null
2058
+
2059
+ const schema = { ...baseSchema }
2060
2060
  if (idProperty && idProperty !== 'id' && !schema[idProperty]) {
2061
- schema[idProperty] = { type: 'number' };
2061
+ schema[idProperty] = { type: 'number' }
2062
2062
  }
2063
2063
 
2064
2064
  scopeOptionsRegistry.set(scopeName, {
@@ -2068,7 +2068,7 @@ export const RestApiAnyapiKnexPlugin = {
2068
2068
  tableName,
2069
2069
  idProperty,
2070
2070
  canonicalFieldsMap,
2071
- });
2071
+ })
2072
2072
 
2073
2073
  const descriptor = await registry.registerResource({
2074
2074
  tenant: tenantId,
@@ -2076,11 +2076,11 @@ export const RestApiAnyapiKnexPlugin = {
2076
2076
  schema,
2077
2077
  relationships,
2078
2078
  canonicalFieldMap: canonicalFieldsMap,
2079
- });
2079
+ })
2080
2080
 
2081
- const updatedStored = scopeOptionsRegistry.get(scopeName) || {};
2082
- updatedStored.canonicalFieldsMap = descriptor.canonicalFieldMap || updatedStored.canonicalFieldsMap || null;
2083
- scopeOptionsRegistry.set(scopeName, updatedStored);
2081
+ const updatedStored = scopeOptionsRegistry.get(scopeName) || {}
2082
+ updatedStored.canonicalFieldsMap = descriptor.canonicalFieldMap || updatedStored.canonicalFieldsMap || null
2083
+ scopeOptionsRegistry.set(scopeName, updatedStored)
2084
2084
 
2085
2085
  if (scope) {
2086
2086
  scope.scopeOptions = {
@@ -2091,31 +2091,31 @@ export const RestApiAnyapiKnexPlugin = {
2091
2091
  tableName,
2092
2092
  idProperty,
2093
2093
  canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
2094
- };
2094
+ }
2095
2095
  }
2096
2096
 
2097
- await rehydrateSchemaInfo(scopeName);
2098
- });
2097
+ await rehydrateSchemaInfo(scopeName)
2098
+ })
2099
2099
 
2100
2100
  addScopeMethod('createKnexTable', async ({ scopeName, scopeOptions }) => {
2101
- const scope = api.resources?.[scopeName] || scopes?.[scopeName];
2102
- const schemaInput = scopeOptions?.schema || scope?.scopeOptions?.schema || {};
2103
- const relationshipInput = scopeOptions?.relationships || scope?.scopeOptions?.relationships || {};
2104
- const storedBefore = scopeOptionsRegistry.get(scopeName) || {};
2105
- const idProperty = scopeOptions?.idProperty
2106
- || scope?.scopeOptions?.idProperty
2107
- || storedBefore.idProperty
2108
- || scope?.vars?.schemaInfo?.idProperty
2109
- || vars.idProperty
2110
- || 'id';
2111
-
2112
- const effectiveSchema = { ...schemaInput };
2101
+ const scope = api.resources?.[scopeName] || scopes?.[scopeName]
2102
+ const schemaInput = scopeOptions?.schema || scope?.scopeOptions?.schema || {}
2103
+ const relationshipInput = scopeOptions?.relationships || scope?.scopeOptions?.relationships || {}
2104
+ const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
2105
+ const idProperty = scopeOptions?.idProperty ||
2106
+ scope?.scopeOptions?.idProperty ||
2107
+ storedBefore.idProperty ||
2108
+ scope?.vars?.schemaInfo?.idProperty ||
2109
+ vars.idProperty ||
2110
+ 'id'
2111
+
2112
+ const effectiveSchema = { ...schemaInput }
2113
2113
  if (idProperty && idProperty !== 'id' && !effectiveSchema[idProperty]) {
2114
- effectiveSchema[idProperty] = { type: 'number' };
2114
+ effectiveSchema[idProperty] = { type: 'number' }
2115
2115
  }
2116
2116
 
2117
2117
  if (scope) {
2118
- const existingOptions = scope.scopeOptions || {};
2118
+ const existingOptions = scope.scopeOptions || {}
2119
2119
  scope.scopeOptions = {
2120
2120
  ...existingOptions,
2121
2121
  schema: {
@@ -2128,7 +2128,7 @@ export const RestApiAnyapiKnexPlugin = {
2128
2128
  ...relationshipInput,
2129
2129
  },
2130
2130
  searchSchema: existingOptions.searchSchema || scopeOptions?.searchSchema,
2131
- };
2131
+ }
2132
2132
  }
2133
2133
  scopeOptionsRegistry.set(scopeName, {
2134
2134
  schema: scope?.scopeOptions?.schema || effectiveSchema || storedBefore.schema || {},
@@ -2137,33 +2137,33 @@ export const RestApiAnyapiKnexPlugin = {
2137
2137
  tableName: scope?.scopeOptions?.tableName || scopeOptions?.tableName || storedBefore.tableName || scopeName,
2138
2138
  idProperty: scope?.scopeOptions?.idProperty || scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
2139
2139
  canonicalFieldsMap: storedBefore.canonicalFieldsMap || null,
2140
- });
2140
+ })
2141
2141
  const descriptor = await registry.registerResource({
2142
2142
  tenant: tenantId,
2143
2143
  resource: scopeName,
2144
2144
  schema: effectiveSchema,
2145
2145
  relationships: relationshipInput,
2146
- canonicalFieldMap: scopeOptions?.canonicalFieldsMap
2147
- || scope?.scopeOptions?.canonicalFieldsMap
2148
- || null,
2149
- });
2146
+ canonicalFieldMap: scopeOptions?.canonicalFieldsMap ||
2147
+ scope?.scopeOptions?.canonicalFieldsMap ||
2148
+ null,
2149
+ })
2150
2150
  if (scope) {
2151
2151
  scope.scopeOptions = {
2152
2152
  ...(scope.scopeOptions || {}),
2153
2153
  canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
2154
- };
2154
+ }
2155
2155
  }
2156
- const storedAfter = scopeOptionsRegistry.get(scopeName) || {};
2157
- storedAfter.canonicalFieldsMap = descriptor.canonicalFieldMap || storedAfter.canonicalFieldsMap || null;
2158
- scopeOptionsRegistry.set(scopeName, storedAfter);
2159
- await rehydrateSchemaInfo(scopeName);
2160
- });
2156
+ const storedAfter = scopeOptionsRegistry.get(scopeName) || {}
2157
+ storedAfter.canonicalFieldsMap = descriptor.canonicalFieldMap || storedAfter.canonicalFieldsMap || null
2158
+ scopeOptionsRegistry.set(scopeName, storedAfter)
2159
+ await rehydrateSchemaInfo(scopeName)
2160
+ })
2161
2161
 
2162
2162
  addScopeMethod('addKnexFields', async ({ scopeName, params }) => {
2163
- if (!params?.fields) return;
2164
- const scope = api.resources?.[scopeName] || scopes?.[scopeName];
2163
+ if (!params?.fields) return
2164
+ const scope = api.resources?.[scopeName] || scopes?.[scopeName]
2165
2165
  if (scope) {
2166
- const existingOptions = scope.scopeOptions || {};
2166
+ const existingOptions = scope.scopeOptions || {}
2167
2167
  scope.scopeOptions = {
2168
2168
  ...existingOptions,
2169
2169
  schema: {
@@ -2171,18 +2171,18 @@ export const RestApiAnyapiKnexPlugin = {
2171
2171
  },
2172
2172
  relationships: existingOptions.relationships || {},
2173
2173
  searchSchema: existingOptions.searchSchema,
2174
- };
2174
+ }
2175
2175
  }
2176
- const storedBefore = scopeOptionsRegistry.get(scopeName) || {};
2177
- const canonicalEntries = params.canonicalFieldsMap ? { ...params.canonicalFieldsMap } : null;
2178
- let latestDescriptor = null;
2176
+ const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
2177
+ const canonicalEntries = params.canonicalFieldsMap ? { ...params.canonicalFieldsMap } : null
2178
+ let latestDescriptor = null
2179
2179
  for (const [fieldName, definition] of Object.entries(params.fields)) {
2180
2180
  if (scope) {
2181
- scope.scopeOptions.schema[fieldName] = definition;
2181
+ scope.scopeOptions.schema[fieldName] = definition
2182
2182
  }
2183
- const canonicalEntry = canonicalEntries ? canonicalEntries[fieldName] : null;
2183
+ const canonicalEntry = canonicalEntries ? canonicalEntries[fieldName] : null
2184
2184
  if (canonicalEntries) {
2185
- delete canonicalEntries[fieldName];
2185
+ delete canonicalEntries[fieldName]
2186
2186
  }
2187
2187
  latestDescriptor = await registry.allocateField({
2188
2188
  tenant: tenantId,
@@ -2190,20 +2190,20 @@ export const RestApiAnyapiKnexPlugin = {
2190
2190
  fieldName,
2191
2191
  definition,
2192
2192
  canonicalField: canonicalEntry,
2193
- });
2193
+ })
2194
2194
  }
2195
2195
  if (canonicalEntries && Object.keys(canonicalEntries).length > 0) {
2196
- const unknown = Object.keys(canonicalEntries).join(', ');
2197
- throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`);
2196
+ const unknown = Object.keys(canonicalEntries).join(', ')
2197
+ throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`)
2198
2198
  }
2199
2199
  if (scope && latestDescriptor?.canonicalFieldMap) {
2200
- scope.scopeOptions.canonicalFieldsMap = latestDescriptor.canonicalFieldMap;
2200
+ scope.scopeOptions.canonicalFieldsMap = latestDescriptor.canonicalFieldMap
2201
2201
  }
2202
2202
  if (scope && params.searchSchema) {
2203
2203
  scope.scopeOptions.searchSchema = {
2204
2204
  ...(scope.scopeOptions.searchSchema || {}),
2205
2205
  ...params.searchSchema,
2206
- };
2206
+ }
2207
2207
  }
2208
2208
  const updatedStored = {
2209
2209
  schema: scope?.scopeOptions?.schema || storedBefore.schema || {},
@@ -2212,74 +2212,74 @@ export const RestApiAnyapiKnexPlugin = {
2212
2212
  tableName: scope?.scopeOptions?.tableName || storedBefore.tableName || scopeName,
2213
2213
  idProperty: scope?.scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
2214
2214
  canonicalFieldsMap: scope?.scopeOptions?.canonicalFieldsMap || storedBefore.canonicalFieldsMap || null,
2215
- };
2216
- scopeOptionsRegistry.set(scopeName, updatedStored);
2217
- await rehydrateSchemaInfo(scopeName);
2218
- });
2215
+ }
2216
+ scopeOptionsRegistry.set(scopeName, updatedStored)
2217
+ await rehydrateSchemaInfo(scopeName)
2218
+ })
2219
2219
 
2220
2220
  addScopeMethod('alterKnexFields', async () => {
2221
- throw new Error('alterKnexFields is not supported by AnyAPI Knex plugin yet');
2222
- });
2221
+ throw new Error('alterKnexFields is not supported by AnyAPI Knex plugin yet')
2222
+ })
2223
2223
  },
2224
- };
2224
+ }
2225
2225
 
2226
2226
  const translateAttributesForStorage = (attributes, descriptor) => {
2227
- const row = {};
2227
+ const row = {}
2228
2228
  for (const [fieldName, value] of Object.entries(attributes)) {
2229
- const slot = descriptor.fields[fieldName];
2230
- if (!slot) continue;
2229
+ const slot = descriptor.fields[fieldName]
2230
+ if (!slot) continue
2231
2231
 
2232
2232
  if (slot.slotType === 'belongsTo') {
2233
- row[slot.slot] = value == null ? null : String(value);
2233
+ row[slot.slot] = value == null ? null : String(value)
2234
2234
  } else {
2235
- row[slot.slot] = value;
2235
+ row[slot.slot] = value
2236
2236
  }
2237
2237
 
2238
2238
  if (slot.slotType === 'belongsTo') {
2239
- const alias = slot.alias || descriptor.fields[fieldName]?.alias;
2240
- const belongsToInfo = alias ? descriptor.belongsTo?.[alias] : null;
2239
+ const alias = slot.alias || descriptor.fields[fieldName]?.alias
2240
+ const belongsToInfo = alias ? descriptor.belongsTo?.[alias] : null
2241
2241
  if (belongsToInfo) {
2242
- row[belongsToInfo.typeColumn] = value == null ? null : belongsToInfo.target;
2242
+ row[belongsToInfo.typeColumn] = value == null ? null : belongsToInfo.target
2243
2243
  }
2244
2244
  }
2245
2245
  }
2246
- return row;
2247
- };
2246
+ return row
2247
+ }
2248
2248
 
2249
2249
  const translateRecordFromStorage = (row, descriptor) => {
2250
- const attributes = {};
2250
+ const attributes = {}
2251
2251
  for (const [slot, logical] of Object.entries(descriptor.reverseAttributes)) {
2252
2252
  if (slot in row) {
2253
- attributes[logical] = row[slot];
2253
+ attributes[logical] = row[slot]
2254
2254
  }
2255
2255
  }
2256
2256
 
2257
- const relationships = {};
2257
+ const relationships = {}
2258
2258
  for (const [alias, info] of Object.entries(descriptor.belongsTo || {})) {
2259
- const idValue = row[info.idColumn];
2259
+ const idValue = row[info.idColumn]
2260
2260
  const relationshipData = idValue == null
2261
2261
  ? null
2262
- : { type: info.target, id: String(idValue) };
2262
+ : { type: info.target, id: String(idValue) }
2263
2263
 
2264
- relationships[alias] = { data: relationshipData };
2264
+ relationships[alias] = { data: relationshipData }
2265
2265
  }
2266
2266
 
2267
2267
  for (const [alias, info] of Object.entries(descriptor.polymorphicBelongsTo || {})) {
2268
- const typeValue = info.typeColumn ? row[info.typeColumn] : null;
2269
- const idValue = info.idColumn ? row[info.idColumn] : null;
2268
+ const typeValue = info.typeColumn ? row[info.typeColumn] : null
2269
+ const idValue = info.idColumn ? row[info.idColumn] : null
2270
2270
 
2271
- let relationshipData = null;
2271
+ let relationshipData = null
2272
2272
  if (typeValue != null && idValue != null) {
2273
2273
  relationshipData = {
2274
2274
  type: String(typeValue),
2275
2275
  id: String(idValue),
2276
- };
2276
+ }
2277
2277
  } else if (typeValue == null && idValue == null) {
2278
- relationshipData = null;
2278
+ relationshipData = null
2279
2279
  }
2280
2280
 
2281
- relationships[alias] = { data: relationshipData };
2281
+ relationships[alias] = { data: relationshipData }
2282
2282
  }
2283
2283
 
2284
- return { attributes, relationships };
2285
- };
2284
+ return { attributes, relationships }
2285
+ }