json-rest-api 1.0.14 → 1.0.16

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 +11 -2
  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 +1 -1
  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 +99 -99
  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 +11 -15
  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,155 +1,154 @@
1
1
  export const MultiHomePlugin = {
2
- name: 'multihome',
3
- dependencies: ['rest-api'],
2
+ name: 'multihome',
3
+ dependencies: ['rest-api'],
4
4
 
5
- install({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
5
+ install ({ api, addHook, vars, helpers, log, scopes, pluginOptions }) {
6
+ if (!api.knex?.instance) {
7
+ throw new Error('Multihome plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)')
8
+ }
9
+
10
+ // Get configuration - hooked-api namespaces options by plugin name
11
+ const multihomeOptions = pluginOptions || {}
6
12
 
7
- if (!api.knex?.instance) {
8
- throw new Error('Multihome plugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex)');
13
+ // Store configuration in vars (data only)
14
+ vars.multihome = {
15
+ field: multihomeOptions.field || 'multihome_id',
16
+ excludeResources: multihomeOptions.excludeResources || ['system_migrations', 'system_logs'],
17
+ requireAuth: multihomeOptions.requireAuth !== undefined ? multihomeOptions.requireAuth : true,
18
+ allowMissing: multihomeOptions.allowMissing || false
19
+ }
20
+
21
+ const resolveStorageAdapter = (scopeName, hookContext = {}) => {
22
+ if (!scopeName) return null
23
+ if (hookContext.storageAdapter) return hookContext.storageAdapter
24
+ if (hookContext.knexQuery?.storageAdapter) return hookContext.knexQuery.storageAdapter
25
+ if (helpers.getStorageAdapter) {
26
+ return helpers.getStorageAdapter(scopeName)
9
27
  }
28
+ return null
29
+ }
10
30
 
11
- // Get configuration - hooked-api namespaces options by plugin name
12
- const multihomeOptions = pluginOptions || {};
13
-
14
- // Store configuration in vars (data only)
15
- vars.multihome = {
16
- field: multihomeOptions.field || 'multihome_id',
17
- excludeResources: multihomeOptions.excludeResources || ['system_migrations', 'system_logs'],
18
- requireAuth: multihomeOptions.requireAuth !== undefined ? multihomeOptions.requireAuth : true,
19
- allowMissing: multihomeOptions.allowMissing || false
20
- };
21
-
22
- const resolveStorageAdapter = (scopeName, hookContext = {}) => {
23
- if (!scopeName) return null;
24
- if (hookContext.storageAdapter) return hookContext.storageAdapter;
25
- if (hookContext.knexQuery?.storageAdapter) return hookContext.knexQuery.storageAdapter;
26
- if (helpers.getStorageAdapter) {
27
- return helpers.getStorageAdapter(scopeName);
28
- }
29
- return null;
30
- };
31
-
32
- const translateColumnReference = ({ field, tableName, scopeName, hookContext }) => {
33
- const storageAdapter = resolveStorageAdapter(scopeName, hookContext);
34
- const translated = storageAdapter?.translateColumn?.(field) ?? field;
35
- if (tableName) {
36
- return `${tableName}.${translated}`;
37
- }
38
- return translated;
39
- };
40
-
41
- const translateFilterValue = ({ field, value, scopeName, hookContext }) => {
42
- const storageAdapter = resolveStorageAdapter(scopeName, hookContext);
43
- if (!storageAdapter?.translateFilterValue) return value;
44
- return storageAdapter.translateFilterValue(field, value);
45
- };
46
-
47
- // Default extractor function
48
- function defaultSubdomainExtractor(request) {
49
- // Extract from subdomain (e.g., 'mobily' from 'mobily.app.com')
50
- const host = request.headers?.host || request.hostname || '';
51
- const subdomain = host.split('.')[0];
52
-
53
- // Don't use 'www' or 'api' as tenant IDs
54
- if (subdomain && !['www', 'api', 'app'].includes(subdomain)) {
55
- return subdomain;
56
- }
31
+ const translateColumnReference = ({ field, tableName, scopeName, hookContext }) => {
32
+ const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
33
+ const translated = storageAdapter?.translateColumn?.(field) ?? field
34
+ if (tableName) {
35
+ return `${tableName}.${translated}`
36
+ }
37
+ return translated
38
+ }
39
+
40
+ const translateFilterValue = ({ field, value, scopeName, hookContext }) => {
41
+ const storageAdapter = resolveStorageAdapter(scopeName, hookContext)
42
+ if (!storageAdapter?.translateFilterValue) return value
43
+ return storageAdapter.translateFilterValue(field, value)
44
+ }
57
45
 
58
- // Fallback to header
59
- return request.headers?.['x-multihome-id'] || null;
46
+ // Default extractor function
47
+ function defaultSubdomainExtractor (request) {
48
+ // Extract from subdomain (e.g., 'mobily' from 'mobily.app.com')
49
+ const host = request.headers?.host || request.hostname || ''
50
+ const subdomain = host.split('.')[0]
51
+
52
+ // Don't use 'www' or 'api' as tenant IDs
53
+ if (subdomain && !['www', 'api', 'app'].includes(subdomain)) {
54
+ return subdomain
60
55
  }
61
56
 
62
- // Store extractor function in helpers
63
- helpers.extractMultihomeId = multihomeOptions.extractor || defaultSubdomainExtractor;
64
-
65
- // Hook into transport layer to extract multihome_id
66
- addHook('transport:request', 'extract-multihome-id', {}, async ({ context, request, helpers }) => {
67
- // Extract multihome_id using configured extractor
68
- const multihomeId = helpers.extractMultihomeId(request);
69
-
70
- if (multihomeId) {
71
- // Store in auth context (even if no user auth)
72
- context.auth = context.auth || {};
73
- context.auth.multihome_id = multihomeId;
74
-
75
- log.debug('Multihome ID extracted', {
76
- multihome_id: multihomeId,
77
- source: request.headers?.host
78
- });
79
- } else if (vars.multihome.requireAuth) {
80
- log.warn('No multihome ID found in request', {
81
- host: request.headers?.host,
82
- headers: Object.keys(request.headers || {})
83
- });
84
- }
85
- });
57
+ // Fallback to header
58
+ return request.headers?.['x-multihome-id'] || null
59
+ }
86
60
 
87
- // Validate that resources have multihome field when added
88
- addHook('scope:added', 'validate-multihome-field', {}, ({ context, vars }) => {
89
- const { scopeName, scopeOptions } = context;
61
+ // Store extractor function in helpers
62
+ helpers.extractMultihomeId = multihomeOptions.extractor || defaultSubdomainExtractor
90
63
 
91
- // Skip excluded resources
92
- if (vars.multihome.excludeResources.includes(scopeName)) {
93
- log.debug(`Resource ${scopeName} excluded from multihome validation`);
94
- return;
95
- }
64
+ // Hook into transport layer to extract multihome_id
65
+ addHook('transport:request', 'extract-multihome-id', {}, async ({ context, request, helpers }) => {
66
+ // Extract multihome_id using configured extractor
67
+ const multihomeId = helpers.extractMultihomeId(request)
96
68
 
97
- // Check if schema has multihome field
98
- const schema = scopeOptions.schema;
99
- if (schema && !schema[vars.multihome.field]) {
100
- if (vars.multihome.allowMissing) {
101
- log.warn(`Resource ${scopeName} missing ${vars.multihome.field} field - multihome filtering disabled for this
102
- resource`);
103
- } else {
104
- throw new Error(
69
+ if (multihomeId) {
70
+ // Store in auth context (even if no user auth)
71
+ context.auth = context.auth || {}
72
+ context.auth.multihome_id = multihomeId
73
+
74
+ log.debug('Multihome ID extracted', {
75
+ multihome_id: multihomeId,
76
+ source: request.headers?.host
77
+ })
78
+ } else if (vars.multihome.requireAuth) {
79
+ log.warn('No multihome ID found in request', {
80
+ host: request.headers?.host,
81
+ headers: Object.keys(request.headers || {})
82
+ })
83
+ }
84
+ })
85
+
86
+ // Validate that resources have multihome field when added
87
+ addHook('scope:added', 'validate-multihome-field', {}, ({ context, vars }) => {
88
+ const { scopeName, scopeOptions } = context
89
+
90
+ // Skip excluded resources
91
+ if (vars.multihome.excludeResources.includes(scopeName)) {
92
+ log.debug(`Resource ${scopeName} excluded from multihome validation`)
93
+ return
94
+ }
95
+
96
+ // Check if schema has multihome field
97
+ const schema = scopeOptions.schema
98
+ if (schema && !schema[vars.multihome.field]) {
99
+ if (vars.multihome.allowMissing) {
100
+ log.warn(`Resource ${scopeName} missing ${vars.multihome.field} field - multihome filtering disabled for this
101
+ resource`)
102
+ } else {
103
+ throw new Error(
105
104
  `Resource '${scopeName}' must have '${vars.multihome.field}' field in schema for multi-tenancy`
106
- );
107
- }
108
- }
109
- });
110
-
111
- // Main filtering hook - adds WHERE clause to all queries
112
- log.debug('Registering multihome filter hook');
113
- addHook('knexQueryFiltering', 'multihome-filter', {}, async ({ context, vars }) => {
114
- // Get query info from context
115
- const { query, tableName, scopeName } = context.knexQuery;
116
-
117
- // Skip excluded resources first
118
- if (vars.multihome.excludeResources.includes(scopeName)) {
119
- log.trace(`Skipping multihome filter for excluded resource: ${scopeName}`);
120
- return;
105
+ )
121
106
  }
107
+ }
108
+ })
109
+
110
+ // Main filtering hook - adds WHERE clause to all queries
111
+ log.debug('Registering multihome filter hook')
112
+ addHook('knexQueryFiltering', 'multihome-filter', {}, async ({ context, vars }) => {
113
+ // Get query info from context
114
+ const { query, tableName, scopeName } = context.knexQuery
115
+
116
+ // Skip excluded resources first
117
+ if (vars.multihome.excludeResources.includes(scopeName)) {
118
+ log.trace(`Skipping multihome filter for excluded resource: ${scopeName}`)
119
+ return
120
+ }
122
121
 
123
122
  // Skip if no multihome context
124
123
  if (!context.auth?.multihome_id) {
125
124
  if (vars.multihome.requireAuth) {
126
- throw new Error('No multihome context available - cannot execute query');
125
+ throw new Error('No multihome context available - cannot execute query')
127
126
  }
128
- return;
127
+ return
129
128
  }
130
129
 
131
130
  log.debug('Applying multihome tenant filter', {
132
131
  scopeName,
133
132
  tableName,
134
133
  tenant: context.auth.multihome_id,
135
- });
134
+ })
136
135
 
137
136
  // Check if this resource has multihome field
138
- const scope = scopes[scopeName];
139
- const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
140
-
141
- if (!hasMultihomeField) {
142
- if (vars.multihome.allowMissing) {
143
- log.trace(`Resource ${scopeName} has no ${vars.multihome.field} field - skipping filter`);
144
- return;
145
- } else {
146
- throw new Error(`Resource ${scopeName} missing required ${vars.multihome.field} field`);
147
- }
137
+ const scope = scopes[scopeName]
138
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field]
139
+
140
+ if (!hasMultihomeField) {
141
+ if (vars.multihome.allowMissing) {
142
+ log.trace(`Resource ${scopeName} has no ${vars.multihome.field} field - skipping filter`)
143
+ return
144
+ } else {
145
+ throw new Error(`Resource ${scopeName} missing required ${vars.multihome.field} field`)
148
146
  }
147
+ }
149
148
 
150
- const storageAdapter = resolveStorageAdapter(scopeName, context);
149
+ const storageAdapter = resolveStorageAdapter(scopeName, context)
151
150
  if (storageAdapter && !context.storageAdapter) {
152
- context.storageAdapter = storageAdapter;
151
+ context.storageAdapter = storageAdapter
153
152
  }
154
153
 
155
154
  const columnRef = translateColumnReference({
@@ -157,162 +156,159 @@ export const MultiHomePlugin = {
157
156
  tableName,
158
157
  scopeName,
159
158
  hookContext: context,
160
- });
159
+ })
161
160
 
162
161
  const tenantValue = translateFilterValue({
163
162
  field: vars.multihome.field,
164
163
  value: context.auth.multihome_id,
165
164
  scopeName,
166
165
  hookContext: context,
167
- });
166
+ })
168
167
 
169
-
170
168
  if (tenantValue === null) {
171
- query.whereNull(columnRef);
169
+ query.whereNull(columnRef)
172
170
  } else {
173
- query.where(columnRef, tenantValue);
171
+ query.where(columnRef, tenantValue)
174
172
  }
175
173
 
176
- log.trace('Added multihome filter', {
177
- scopeName,
178
- tableName,
179
- multihome_id: context.auth.multihome_id
180
- });
181
- });
182
-
183
- // Set multihome_id on new records
184
- addHook('beforeSchemaValidate', 'set-multihome-id', {}, async ({ context, scopeName, vars }) => {
185
-
186
- // Skip excluded resources first
187
- if (vars.multihome.excludeResources.includes(scopeName)) {
188
- return;
189
- }
174
+ log.trace('Added multihome filter', {
175
+ scopeName,
176
+ tableName,
177
+ multihome_id: context.auth.multihome_id
178
+ })
179
+ })
180
+
181
+ // Set multihome_id on new records
182
+ addHook('beforeSchemaValidate', 'set-multihome-id', {}, async ({ context, scopeName, vars }) => {
183
+ // Skip excluded resources first
184
+ if (vars.multihome.excludeResources.includes(scopeName)) {
185
+ return
186
+ }
190
187
 
191
- // Skip if no multihome context
192
- if (!context.auth?.multihome_id) {
193
- if (vars.multihome.requireAuth) {
194
- throw new Error('Cannot create record without multihome context');
195
- }
196
- return;
188
+ // Skip if no multihome context
189
+ if (!context.auth?.multihome_id) {
190
+ if (vars.multihome.requireAuth) {
191
+ throw new Error('Cannot create record without multihome context')
197
192
  }
193
+ return
194
+ }
198
195
 
199
- // Check if this resource has multihome field
200
- const scope = scopes[scopeName];
201
- const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
196
+ // Check if this resource has multihome field
197
+ const scope = scopes[scopeName]
198
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field]
202
199
 
203
- if (!hasMultihomeField) {
204
- if (!vars.multihome.allowMissing) {
205
- throw new Error(`Resource ${scopeName} missing required ${vars.multihome.field} field`);
206
- }
207
- return;
200
+ if (!hasMultihomeField) {
201
+ if (!vars.multihome.allowMissing) {
202
+ throw new Error(`Resource ${scopeName} missing required ${vars.multihome.field} field`)
208
203
  }
204
+ return
205
+ }
209
206
 
210
- // For POST, always set multihome_id
211
- if (context.method === 'post') {
212
- context.inputRecord.data.attributes = context.inputRecord.data.attributes || {};
213
- context.inputRecord.data.attributes[vars.multihome.field] = context.auth.multihome_id;
207
+ // For POST, always set multihome_id
208
+ if (context.method === 'post') {
209
+ context.inputRecord.data.attributes = context.inputRecord.data.attributes || {}
210
+ context.inputRecord.data.attributes[vars.multihome.field] = context.auth.multihome_id
214
211
 
215
- log.debug('Set multihome_id on new record', {
216
- scopeName,
217
- multihome_id: context.auth.multihome_id
218
- });
219
- }
212
+ log.debug('Set multihome_id on new record', {
213
+ scopeName,
214
+ multihome_id: context.auth.multihome_id
215
+ })
216
+ }
220
217
 
221
- // For PUT/PATCH, validate multihome_id if provided
222
- if ((context.method === 'put' || context.method === 'patch') &&
218
+ // For PUT/PATCH, validate multihome_id if provided
219
+ if ((context.method === 'put' || context.method === 'patch') &&
223
220
  context.inputRecord.data.attributes?.[vars.multihome.field]) {
224
-
225
- const providedId = context.inputRecord.data.attributes[vars.multihome.field];
226
- if (providedId !== context.auth.multihome_id) {
227
- throw new Error(
221
+ const providedId = context.inputRecord.data.attributes[vars.multihome.field]
222
+ if (providedId !== context.auth.multihome_id) {
223
+ throw new Error(
228
224
  `Cannot set ${vars.multihome.field} to '${providedId}' - must match current context '${context.auth.multihome_id}'`
229
- );
230
- }
231
- }
232
- });
233
-
234
- // Add checkPermissions hook to enforce tenant isolation
235
- // This is the single source of truth for single-record access control
236
- addHook('checkPermissions', 'multihome-check-permissions', {}, async ({ context, scopeName }) => {
237
- // Extract the needed values from originalContext
238
- const auth = context.originalContext?.auth;
239
- const minimalRecord = context.originalContext?.minimalRecord;
240
- const id = context.originalContext?.id;
241
-
242
- // Skip excluded resources
243
- if (vars.multihome.excludeResources.includes(scopeName)) {
244
- return;
225
+ )
245
226
  }
227
+ }
228
+ })
229
+
230
+ // Add checkPermissions hook to enforce tenant isolation
231
+ // This is the single source of truth for single-record access control
232
+ addHook('checkPermissions', 'multihome-check-permissions', {}, async ({ context, scopeName }) => {
233
+ // Extract the needed values from originalContext
234
+ const auth = context.originalContext?.auth
235
+ const minimalRecord = context.originalContext?.minimalRecord
236
+ const id = context.originalContext?.id
237
+
238
+ // Skip excluded resources
239
+ if (vars.multihome.excludeResources.includes(scopeName)) {
240
+ return
241
+ }
246
242
 
247
- // Skip if no multihome context
248
- if (!auth?.multihome_id) {
249
- if (vars.multihome.requireAuth) {
250
- throw new Error('No multihome context available');
251
- }
252
- return;
243
+ // Skip if no multihome context
244
+ if (!auth?.multihome_id) {
245
+ if (vars.multihome.requireAuth) {
246
+ throw new Error('No multihome context available')
253
247
  }
248
+ return
249
+ }
254
250
 
255
- // Get the scope to check if it has multihome field
256
- const scope = scopes[scopeName];
257
- const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field];
251
+ // Get the scope to check if it has multihome field
252
+ const scope = scopes[scopeName]
253
+ const hasMultihomeField = scope?.vars?.schemaInfo?.schemaStructure?.[vars.multihome.field]
258
254
 
259
- if (!hasMultihomeField) {
260
- return; // Resource doesn't support multihome
261
- }
255
+ if (!hasMultihomeField) {
256
+ return // Resource doesn't support multihome
257
+ }
258
+
259
+ // For operations on existing records, verify tenant ownership
260
+ if (minimalRecord) {
261
+ // minimalRecord is in JSON:API format, so tenant_id is in attributes
262
+ const recordTenant = minimalRecord.attributes?.[vars.multihome.field]
263
+ const userTenant = auth.multihome_id
262
264
 
263
- // For operations on existing records, verify tenant ownership
264
- if (minimalRecord) {
265
- // minimalRecord is in JSON:API format, so tenant_id is in attributes
266
- const recordTenant = minimalRecord.attributes?.[vars.multihome.field];
267
- const userTenant = auth.multihome_id;
268
-
269
- if (recordTenant !== userTenant) {
270
- log.error('Multihome permission violation', {
271
- scopeName,
272
- recordId: id,
273
- recordTenant: recordTenant,
274
- userTenant: userTenant,
275
- method: context.method
276
- });
277
-
278
- // Return 404 for GET to prevent information leakage
279
- // Return 403 for other operations
280
- if (context.method === 'get') {
281
- const error = new Error('Resource not found');
282
- error.code = 'REST_API_RESOURCE';
283
- throw error;
284
- } else {
285
- const error = new Error('Access denied - insufficient permissions');
286
- error.code = 'REST_API_FORBIDDEN';
287
- throw error;
288
- }
265
+ if (recordTenant !== userTenant) {
266
+ log.error('Multihome permission violation', {
267
+ scopeName,
268
+ recordId: id,
269
+ recordTenant,
270
+ userTenant,
271
+ method: context.method
272
+ })
273
+
274
+ // Return 404 for GET to prevent information leakage
275
+ // Return 403 for other operations
276
+ if (context.method === 'get') {
277
+ const error = new Error('Resource not found')
278
+ error.code = 'REST_API_RESOURCE'
279
+ throw error
280
+ } else {
281
+ const error = new Error('Access denied - insufficient permissions')
282
+ error.code = 'REST_API_FORBIDDEN'
283
+ throw error
289
284
  }
290
285
  }
291
- });
292
-
293
- // Note: PUT, PATCH, and DELETE operations will fail naturally if the record
294
- // doesn't exist due to tenant filtering in the GET operation that precedes them
295
-
296
- // Add API method to get current multihome context
297
- api.multihome = {
298
- getCurrentTenant: () => {
299
- // This would need to be called within a request context
300
- // Real implementation would need access to current request context
301
- return null;
302
- },
303
-
304
- // Runtime configuration helper (for debugging/testing)
305
- getConfig: () => ({
306
- ...vars.multihome,
307
- hasCustomExtractor: helpers.extractMultihomeId !== defaultSubdomainExtractor
308
- })
309
- };
310
-
311
- log.info('MultiHome plugin installed', {
312
- field: vars.multihome.field,
313
- excludedResources: vars.multihome.excludeResources,
314
- requireAuth: vars.multihome.requireAuth,
286
+ }
287
+ })
288
+
289
+ // Note: PUT, PATCH, and DELETE operations will fail naturally if the record
290
+ // doesn't exist due to tenant filtering in the GET operation that precedes them
291
+
292
+ // Add API method to get current multihome context
293
+ api.multihome = {
294
+ getCurrentTenant: () => {
295
+ // This would need to be called within a request context
296
+ // Real implementation would need access to current request context
297
+ return null
298
+ },
299
+
300
+ // Runtime configuration helper (for debugging/testing)
301
+ getConfig: () => ({
302
+ ...vars.multihome,
315
303
  hasCustomExtractor: helpers.extractMultihomeId !== defaultSubdomainExtractor
316
- });
304
+ })
317
305
  }
318
- };
306
+
307
+ log.info('MultiHome plugin installed', {
308
+ field: vars.multihome.field,
309
+ excludedResources: vars.multihome.excludeResources,
310
+ requireAuth: vars.multihome.requireAuth,
311
+ hasCustomExtractor: helpers.extractMultihomeId !== defaultSubdomainExtractor
312
+ })
313
+ }
314
+ }
@@ -1,7 +1,7 @@
1
- import { compileSchemas } from '../lib/querying-writing/compile-schemas.js';
1
+ import { compileSchemas } from '../lib/querying-writing/compile-schemas.js'
2
2
 
3
- export default async function compileResourceSchemas({ context, scopes, runHooks }) {
4
- const scope = scopes[context.scopeName];
3
+ export default async function compileResourceSchemas ({ context, scopes, runHooks }) {
4
+ const scope = scopes[context.scopeName]
5
5
  // Pass scopeOptions and vars from context since scope object structure is different
6
- return compileSchemas({ ...scope, scopeOptions: context.scopeOptions, vars: context.vars }, { context: { scopeName: context.scopeName }, runHooks });
7
- }
6
+ return compileSchemas({ ...scope, scopeOptions: context.scopeOptions, vars: context.vars }, { context: { scopeName: context.scopeName }, runHooks })
7
+ }