json-rest-api 1.0.14 → 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.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,155 +1,154 @@
|
|
|
1
1
|
export const MultiHomePlugin = {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
name: 'multihome',
|
|
3
|
+
dependencies: ['rest-api'],
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
//
|
|
63
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
const { scopeName, scopeOptions } = context;
|
|
61
|
+
// Store extractor function in helpers
|
|
62
|
+
helpers.extractMultihomeId = multihomeOptions.extractor || defaultSubdomainExtractor
|
|
90
63
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|