json-rest-api 1.0.24 → 1.0.26
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/README.md +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +605 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +36 -26
- package/plugins/core/lib/anyapi/schema-utils.js +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/storage-adapter-utils.js +8 -8
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +6 -1
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +383 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +489 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +1 -0
- package/tests/test-development-guide.md +5 -5
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
commitOwnedTransaction,
|
|
3
|
+
handleWriteMethodError,
|
|
4
|
+
validateRelationshipRoutePayload
|
|
5
|
+
} from './common.js'
|
|
2
6
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
3
7
|
|
|
4
8
|
/**
|
|
@@ -19,6 +23,7 @@ export default async function patchRelationshipMethod ({ params, context, vars,
|
|
|
19
23
|
scopeName
|
|
20
24
|
})
|
|
21
25
|
context.relationshipName = params.relationshipName
|
|
26
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
22
27
|
|
|
23
28
|
// Transaction handling
|
|
24
29
|
context.transaction = params.transaction ||
|
|
@@ -31,6 +36,14 @@ export default async function patchRelationshipMethod ({ params, context, vars,
|
|
|
31
36
|
await runHooks('checkPermissions')
|
|
32
37
|
await runHooks('checkPermissionsPatchRelationship')
|
|
33
38
|
|
|
39
|
+
const relationshipData = validateRelationshipRoutePayload({
|
|
40
|
+
context,
|
|
41
|
+
vars,
|
|
42
|
+
scopeName,
|
|
43
|
+
operation: 'patchRelationship',
|
|
44
|
+
relationshipData: params.relationshipData
|
|
45
|
+
})
|
|
46
|
+
|
|
34
47
|
// Reuse existing patch with relationship data
|
|
35
48
|
await scope.patch({
|
|
36
49
|
id: context.id,
|
|
@@ -39,21 +52,19 @@ export default async function patchRelationshipMethod ({ params, context, vars,
|
|
|
39
52
|
type: scopeName,
|
|
40
53
|
id: context.id,
|
|
41
54
|
relationships: {
|
|
42
|
-
[params.relationshipName]: { data:
|
|
55
|
+
[params.relationshipName]: { data: relationshipData }
|
|
43
56
|
}
|
|
44
57
|
}
|
|
45
58
|
},
|
|
46
59
|
transaction: context.transaction,
|
|
47
60
|
simplified: false,
|
|
48
61
|
isTransport: params.isTransport
|
|
49
|
-
})
|
|
62
|
+
}, { ...context })
|
|
50
63
|
|
|
51
64
|
await runHooks('finish')
|
|
52
65
|
await runHooks('finishPatchRelationship')
|
|
53
66
|
|
|
54
|
-
|
|
55
|
-
await context.transaction.commit()
|
|
56
|
-
}
|
|
67
|
+
await commitOwnedTransaction(context, runHooks)
|
|
57
68
|
|
|
58
69
|
// 204 No Content
|
|
59
70
|
} catch (error) {
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
applyFieldSetters,
|
|
15
15
|
validatePivotResource,
|
|
16
16
|
handleRecordReturnAfterWrite,
|
|
17
|
-
handleWriteMethodError
|
|
17
|
+
handleWriteMethodError,
|
|
18
|
+
commitOwnedTransaction
|
|
18
19
|
} from './common.js'
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -222,11 +223,7 @@ export default async function patchMethod ({
|
|
|
222
223
|
log
|
|
223
224
|
})
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
if (context.shouldCommit) {
|
|
227
|
-
await context.transaction.commit()
|
|
228
|
-
await runHooks('afterCommit')
|
|
229
|
-
}
|
|
226
|
+
await commitOwnedTransaction(context, runHooks)
|
|
230
227
|
|
|
231
228
|
return ret
|
|
232
229
|
} catch (error) {
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { RestApiResourceError, RestApiValidationError
|
|
2
|
-
import {
|
|
1
|
+
import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
import {
|
|
3
|
+
commitOwnedTransaction,
|
|
4
|
+
findRelationshipDefinition,
|
|
5
|
+
getVisibleRelationshipParent,
|
|
6
|
+
handleWriteMethodError,
|
|
7
|
+
validateRelationshipRoutePayload
|
|
8
|
+
} from './common.js'
|
|
3
9
|
import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
4
10
|
import {
|
|
5
11
|
normalizeRelationshipIdentifiers,
|
|
@@ -50,22 +56,28 @@ export default async function postRelationshipMethod ({ params, context, vars, h
|
|
|
50
56
|
)
|
|
51
57
|
}
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
params.relationshipData = validateRelationshipRoutePayload({
|
|
60
|
+
context,
|
|
61
|
+
vars,
|
|
62
|
+
scopeName,
|
|
63
|
+
operation: 'postRelationship',
|
|
64
|
+
relationshipData: params.relationshipData
|
|
65
|
+
})
|
|
56
66
|
params.relationshipData = normalizeRelationshipIdentifiers(params.relationshipData, { api })
|
|
57
67
|
|
|
58
68
|
// Check permissions
|
|
59
69
|
await runHooks('checkPermissions')
|
|
60
70
|
await runHooks('checkPermissionsPostRelationship')
|
|
61
71
|
|
|
62
|
-
// Verify parent exists
|
|
63
|
-
const
|
|
72
|
+
// Verify the parent exists in the caller's visible dataset.
|
|
73
|
+
const parentRecord = await getVisibleRelationshipParent({
|
|
74
|
+
context,
|
|
75
|
+
helpers,
|
|
64
76
|
scopeName,
|
|
65
|
-
|
|
77
|
+
runHooks
|
|
66
78
|
})
|
|
67
79
|
|
|
68
|
-
if (!
|
|
80
|
+
if (!parentRecord) {
|
|
69
81
|
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
70
82
|
}
|
|
71
83
|
|
|
@@ -84,7 +96,7 @@ export default async function postRelationshipMethod ({ params, context, vars, h
|
|
|
84
96
|
through: relDef.through,
|
|
85
97
|
foreignKey: relDef.foreignKey,
|
|
86
98
|
otherKey: relDef.otherKey
|
|
87
|
-
}, params.relationshipData, context.transaction)
|
|
99
|
+
}, params.relationshipData, context.transaction, context)
|
|
88
100
|
}
|
|
89
101
|
} else if (relDef.type === 'hasMany') {
|
|
90
102
|
// Update foreign keys for hasMany
|
|
@@ -101,16 +113,14 @@ export default async function postRelationshipMethod ({ params, context, vars, h
|
|
|
101
113
|
},
|
|
102
114
|
transaction: context.transaction,
|
|
103
115
|
simplified: false
|
|
104
|
-
})
|
|
116
|
+
}, { ...context })
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
await runHooks('finish')
|
|
109
121
|
await runHooks('finishPostRelationship')
|
|
110
122
|
|
|
111
|
-
|
|
112
|
-
await context.transaction.commit()
|
|
113
|
-
}
|
|
123
|
+
await commitOwnedTransaction(context, runHooks)
|
|
114
124
|
|
|
115
125
|
// 204 No Content
|
|
116
126
|
} catch (error) {
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
applyFieldSetters,
|
|
10
10
|
validatePivotResource,
|
|
11
11
|
handleRecordReturnAfterWrite,
|
|
12
|
-
handleWriteMethodError
|
|
12
|
+
handleWriteMethodError,
|
|
13
|
+
commitOwnedTransaction
|
|
13
14
|
} from './common.js'
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -151,7 +152,7 @@ export default async function postMethod ({
|
|
|
151
152
|
|
|
152
153
|
// Validate pivot resource exists
|
|
153
154
|
validatePivotResource(scopes, relDef, relName)
|
|
154
|
-
await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
155
|
+
await createPivotRecords(api, context.id, relDef, relData, context.transaction, context)
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const ret = await handleRecordReturnAfterWrite({
|
|
@@ -168,11 +169,7 @@ export default async function postMethod ({
|
|
|
168
169
|
log
|
|
169
170
|
})
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
if (context.shouldCommit) {
|
|
173
|
-
await context.transaction.commit()
|
|
174
|
-
await runHooks('afterCommit')
|
|
175
|
-
}
|
|
172
|
+
await commitOwnedTransaction(context, runHooks)
|
|
176
173
|
|
|
177
174
|
return ret
|
|
178
175
|
} catch (error) {
|
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
applyFieldSetters,
|
|
16
16
|
validatePivotResource,
|
|
17
17
|
handleRecordReturnAfterWrite,
|
|
18
|
-
handleWriteMethodError
|
|
18
|
+
handleWriteMethodError,
|
|
19
|
+
commitOwnedTransaction
|
|
19
20
|
} from './common.js'
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -274,7 +275,7 @@ export default async function putMethod ({
|
|
|
274
275
|
}
|
|
275
276
|
})
|
|
276
277
|
} else if (relData.length > 0) {
|
|
277
|
-
await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
278
|
+
await createPivotRecords(api, context.id, relDef, relData, context.transaction, context)
|
|
278
279
|
}
|
|
279
280
|
}
|
|
280
281
|
|
|
@@ -292,11 +293,7 @@ export default async function putMethod ({
|
|
|
292
293
|
log
|
|
293
294
|
})
|
|
294
295
|
|
|
295
|
-
|
|
296
|
-
if (context.shouldCommit) {
|
|
297
|
-
await context.transaction.commit()
|
|
298
|
-
await runHooks('afterCommit')
|
|
299
|
-
}
|
|
296
|
+
await commitOwnedTransaction(context, runHooks)
|
|
300
297
|
|
|
301
298
|
return ret
|
|
302
299
|
} catch (error) {
|
|
@@ -21,6 +21,7 @@ import addRouteMethod from './rest-api-plugin-methods/add-route.js'
|
|
|
21
21
|
import releaseMethod from './rest-api-plugin-methods/release.js'
|
|
22
22
|
import { defaultDataHelpers } from './lib/querying-writing/default-data-helpers.js'
|
|
23
23
|
import { DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_INCLUDE_DEPTH_LIMIT } from './lib/querying-writing/knex-constants.js'
|
|
24
|
+
import { normalizeReturnRecordSetting } from './lib/querying-writing/return-record-settings.js'
|
|
24
25
|
|
|
25
26
|
import getRelatedMethod from './rest-api-plugin-methods/get-related.js'
|
|
26
27
|
import postRelationshipMethod from './rest-api-plugin-methods/post-relationship.js'
|
|
@@ -71,42 +72,11 @@ export const RestApiPlugin = {
|
|
|
71
72
|
? restApiOptions.normalizeId
|
|
72
73
|
: defaultNormalizeResourceId
|
|
73
74
|
|
|
74
|
-
// Return
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const optionConfigs = [
|
|
82
|
-
{
|
|
83
|
-
propName: 'returnRecordApi',
|
|
84
|
-
defaultValue: 'full',
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
propName: 'returnRecordTransport',
|
|
88
|
-
defaultValue: 'no',
|
|
89
|
-
},
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
for (const config of optionConfigs) {
|
|
93
|
-
const optionValue = restApiOptions[config.propName]
|
|
94
|
-
let processedValue
|
|
95
|
-
|
|
96
|
-
if (typeof optionValue === 'object' && optionValue !== null) {
|
|
97
|
-
processedValue = {
|
|
98
|
-
post: normalizeReturnValue(optionValue.post, config.defaultValue),
|
|
99
|
-
put: normalizeReturnValue(optionValue.put, config.defaultValue),
|
|
100
|
-
patch: normalizeReturnValue(optionValue.patch, config.defaultValue),
|
|
101
|
-
}
|
|
102
|
-
} else if (optionValue !== undefined) {
|
|
103
|
-
const normalized = normalizeReturnValue(optionValue, config.defaultValue)
|
|
104
|
-
processedValue = { post: normalized, put: normalized, patch: normalized }
|
|
105
|
-
} else {
|
|
106
|
-
processedValue = { post: config.defaultValue, put: config.defaultValue, patch: config.defaultValue }
|
|
107
|
-
}
|
|
108
|
-
vars[config.propName] = processedValue
|
|
109
|
-
}
|
|
75
|
+
// Return record configuration for API and Transport.
|
|
76
|
+
// Preferred values are 'no', 'minimal', and 'full'; booleans are normalized
|
|
77
|
+
// for backwards compatibility.
|
|
78
|
+
vars.returnRecordApi = normalizeReturnRecordSetting(restApiOptions.returnRecordApi, 'full')
|
|
79
|
+
vars.returnRecordTransport = normalizeReturnRecordSetting(restApiOptions.returnRecordTransport, 'no')
|
|
110
80
|
|
|
111
81
|
log.debug('returnRecordApi configuration:', vars.returnRecordApi)
|
|
112
82
|
log.debug('returnRecordTransport configuration:', vars.returnRecordTransport)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createStorageAdapterUtilities } from './lib/querying/storage-adapter-utils.js'
|
|
2
|
+
|
|
3
|
+
const INLINE_POLICY_NAME = '<inline>'
|
|
4
|
+
|
|
5
|
+
function createRowPolicyError (message, code = 'REST_API_ROW_POLICY_CONTRACT') {
|
|
6
|
+
const error = new Error(message)
|
|
7
|
+
error.code = code
|
|
8
|
+
return error
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function compileRowPolicy ({ scopeName, scopeOptions = {}, policies }) {
|
|
12
|
+
const definition = scopeOptions.rowPolicy
|
|
13
|
+
|
|
14
|
+
if (definition === undefined || definition === null || definition === false) {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof definition === 'function') {
|
|
19
|
+
return {
|
|
20
|
+
name: INLINE_POLICY_NAME,
|
|
21
|
+
source: 'inline',
|
|
22
|
+
applyQuery: definition
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof definition === 'string') {
|
|
27
|
+
const name = definition.trim()
|
|
28
|
+
const applyQuery = policies[name]
|
|
29
|
+
|
|
30
|
+
if (!name || !applyQuery) {
|
|
31
|
+
throw createRowPolicyError(
|
|
32
|
+
`Unknown row policy '${definition}' on resource '${scopeName}'.`
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
source: 'registry',
|
|
39
|
+
applyQuery
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
throw createRowPolicyError(
|
|
44
|
+
`Row policy for resource '${scopeName}' must be a registered policy name, a function, or false.`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const RowPolicyPlugin = {
|
|
49
|
+
name: 'row-policy',
|
|
50
|
+
dependencies: ['rest-api', 'rest-api-knex|rest-api-anyapi-knex'],
|
|
51
|
+
|
|
52
|
+
install ({ api, addHook, vars, helpers, log, scopes, pluginOptions = {} }) {
|
|
53
|
+
if (!api.knex?.instance) {
|
|
54
|
+
throw new Error('RowPolicyPlugin requires a storage plugin with knex support (rest-api-knex or rest-api-anyapi-knex).')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const policies = { ...(pluginOptions.policies || {}) }
|
|
58
|
+
for (const [name, policy] of Object.entries(policies)) {
|
|
59
|
+
if (typeof policy !== 'function') {
|
|
60
|
+
throw createRowPolicyError(`Row policy '${name}' must be a function.`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
vars.rowPolicy = {
|
|
65
|
+
policies: Object.keys(policies)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
addHook('scope:added', 'compile-row-policy', {}, ({ context }) => {
|
|
69
|
+
const { scopeName, scopeOptions = {} } = context
|
|
70
|
+
const scope = scopes[scopeName]
|
|
71
|
+
|
|
72
|
+
scope.vars.rowPolicy = compileRowPolicy({
|
|
73
|
+
scopeName,
|
|
74
|
+
scopeOptions,
|
|
75
|
+
policies
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
addHook('knexQueryFiltering', 'apply-row-policy', {}, async (hookParams) => {
|
|
80
|
+
const { context } = hookParams
|
|
81
|
+
const {
|
|
82
|
+
query,
|
|
83
|
+
scopeName,
|
|
84
|
+
tableName,
|
|
85
|
+
db,
|
|
86
|
+
queryPurpose = 'unspecified',
|
|
87
|
+
isAnyApi = false,
|
|
88
|
+
storageAdapter: hookStorageAdapter
|
|
89
|
+
} = context.knexQuery || {}
|
|
90
|
+
|
|
91
|
+
if (!query || !scopeName) return
|
|
92
|
+
|
|
93
|
+
const compiledPolicy = scopes[scopeName]?.vars?.rowPolicy
|
|
94
|
+
if (!compiledPolicy) return
|
|
95
|
+
|
|
96
|
+
const adapterUtils = createStorageAdapterUtilities(hookParams, {
|
|
97
|
+
getStorageAdapter: helpers.getStorageAdapter
|
|
98
|
+
})
|
|
99
|
+
const storageAdapter = hookStorageAdapter || adapterUtils.fetchStorageAdapter(scopeName)
|
|
100
|
+
|
|
101
|
+
const column = (field, options = {}) => {
|
|
102
|
+
const targetScopeName = options.scopeName || scopeName
|
|
103
|
+
const alias = Object.prototype.hasOwnProperty.call(options, 'alias')
|
|
104
|
+
? options.alias
|
|
105
|
+
: adapterUtils.defaultAliasForScope(targetScopeName)
|
|
106
|
+
return adapterUtils.translateColumn(targetScopeName, field, alias)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const value = (field, rawValue, options = {}) => {
|
|
110
|
+
const targetScopeName = options.scopeName || scopeName
|
|
111
|
+
return adapterUtils.translateFilterValue(targetScopeName, field, rawValue)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await compiledPolicy.applyQuery({
|
|
115
|
+
query,
|
|
116
|
+
context,
|
|
117
|
+
scopeName,
|
|
118
|
+
tableName,
|
|
119
|
+
queryPurpose,
|
|
120
|
+
db,
|
|
121
|
+
isAnyApi,
|
|
122
|
+
storageAdapter,
|
|
123
|
+
column,
|
|
124
|
+
value,
|
|
125
|
+
api
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
if (result === false) {
|
|
129
|
+
query.whereRaw('1 = 0')
|
|
130
|
+
} else if (result !== true) {
|
|
131
|
+
throw createRowPolicyError(
|
|
132
|
+
`Row policy '${compiledPolicy.name}' on resource '${scopeName}' must return true after applying its predicate, or false to deny all rows.`
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
api.rowPolicies = {
|
|
138
|
+
getConfig: () => ({
|
|
139
|
+
policies: Object.keys(policies)
|
|
140
|
+
}),
|
|
141
|
+
|
|
142
|
+
getScopeConfig: (scopeName) => {
|
|
143
|
+
const compiledPolicy = scopes[scopeName]?.vars?.rowPolicy
|
|
144
|
+
if (!compiledPolicy) return null
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
policy: compiledPolicy.name,
|
|
148
|
+
source: compiledPolicy.source
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
log.info('Row policy plugin installed', {
|
|
154
|
+
policies: Object.keys(policies)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -19,6 +19,45 @@ export class LocalStorage {
|
|
|
19
19
|
this.maxFilenameLength = options.maxFilenameLength || 255
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
hasPathSegments (filename) {
|
|
23
|
+
return filename.includes('/') ||
|
|
24
|
+
filename.includes('\\') ||
|
|
25
|
+
path.isAbsolute(filename) ||
|
|
26
|
+
path.win32.isAbsolute(filename) ||
|
|
27
|
+
/^[A-Za-z]:/.test(filename)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
validateCustomBasename (basename) {
|
|
31
|
+
if (typeof basename !== 'string') {
|
|
32
|
+
throw new Error('Custom nameGenerator must return a string')
|
|
33
|
+
}
|
|
34
|
+
if (basename.trim().length === 0) {
|
|
35
|
+
throw new Error('Custom nameGenerator must return a non-empty basename')
|
|
36
|
+
}
|
|
37
|
+
if (this.hasPathSegments(basename)) {
|
|
38
|
+
throw new Error('Custom nameGenerator must return a basename, not a path')
|
|
39
|
+
}
|
|
40
|
+
return basename
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
resolveStoragePath (filename) {
|
|
44
|
+
if (typeof filename !== 'string' || filename.trim().length === 0) {
|
|
45
|
+
throw new Error('Invalid file name')
|
|
46
|
+
}
|
|
47
|
+
if (this.hasPathSegments(filename)) {
|
|
48
|
+
throw new Error('Invalid file path')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const resolvedDir = path.resolve(this.directory)
|
|
52
|
+
const resolvedPath = path.resolve(resolvedDir, filename)
|
|
53
|
+
const relative = path.relative(resolvedDir, resolvedPath)
|
|
54
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
55
|
+
throw new Error('Invalid file path')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return resolvedPath
|
|
59
|
+
}
|
|
60
|
+
|
|
22
61
|
/**
|
|
23
62
|
* Generate a safe filename
|
|
24
63
|
*/
|
|
@@ -61,7 +100,7 @@ export class LocalStorage {
|
|
|
61
100
|
if (!this.nameGenerator) {
|
|
62
101
|
throw new Error('nameGenerator function required for custom strategy')
|
|
63
102
|
}
|
|
64
|
-
basename = await this.nameGenerator(file)
|
|
103
|
+
basename = this.validateCustomBasename(await this.nameGenerator(file))
|
|
65
104
|
break
|
|
66
105
|
|
|
67
106
|
default:
|
|
@@ -107,7 +146,7 @@ export class LocalStorage {
|
|
|
107
146
|
* Ensure filename is unique
|
|
108
147
|
*/
|
|
109
148
|
async ensureUnique (filename) {
|
|
110
|
-
const filepath =
|
|
149
|
+
const filepath = this.resolveStoragePath(filename)
|
|
111
150
|
|
|
112
151
|
try {
|
|
113
152
|
await fs.access(filepath)
|
|
@@ -120,7 +159,7 @@ export class LocalStorage {
|
|
|
120
159
|
do {
|
|
121
160
|
newFilename = `${base}_${counter}${ext}`
|
|
122
161
|
counter++
|
|
123
|
-
} while (await this.fileExists(
|
|
162
|
+
} while (await this.fileExists(this.resolveStoragePath(newFilename)))
|
|
124
163
|
|
|
125
164
|
return newFilename
|
|
126
165
|
} catch (error) {
|
|
@@ -171,14 +210,7 @@ export class LocalStorage {
|
|
|
171
210
|
|
|
172
211
|
// Generate secure filename
|
|
173
212
|
const filename = await this.generateFilename(file)
|
|
174
|
-
const filepath =
|
|
175
|
-
|
|
176
|
-
// Ensure we're not writing outside our directory (defense in depth)
|
|
177
|
-
const resolvedPath = path.resolve(filepath)
|
|
178
|
-
const resolvedDir = path.resolve(this.directory)
|
|
179
|
-
if (!resolvedPath.startsWith(resolvedDir)) {
|
|
180
|
-
throw new Error('Invalid file path')
|
|
181
|
-
}
|
|
213
|
+
const filepath = this.resolveStoragePath(filename)
|
|
182
214
|
|
|
183
215
|
// Write file
|
|
184
216
|
if (file.data) {
|
|
@@ -198,14 +230,7 @@ export class LocalStorage {
|
|
|
198
230
|
*/
|
|
199
231
|
async delete (url) {
|
|
200
232
|
const filename = path.basename(url)
|
|
201
|
-
const filepath =
|
|
202
|
-
|
|
203
|
-
// Security check
|
|
204
|
-
const resolvedPath = path.resolve(filepath)
|
|
205
|
-
const resolvedDir = path.resolve(this.directory)
|
|
206
|
-
if (!resolvedPath.startsWith(resolvedDir)) {
|
|
207
|
-
throw new Error('Invalid file path')
|
|
208
|
-
}
|
|
233
|
+
const filepath = this.resolveStoragePath(filename)
|
|
209
234
|
|
|
210
235
|
try {
|
|
211
236
|
await fs.unlink(filepath)
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* S3 Storage Adapter
|
|
2
|
+
* S3 Storage Adapter (mock/demo)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Generates S3-style URLs without uploading files to Amazon S3.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
7
|
* ```javascript
|
|
8
|
-
* import { S3Storage } from '
|
|
8
|
+
* import { S3Storage } from 'json-rest-api';
|
|
9
9
|
*
|
|
10
10
|
* const storage = new S3Storage({
|
|
11
11
|
* bucket: 'my-uploads',
|
|
12
12
|
* region: 'us-east-1',
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* prefix: 'uploads/' // optional path prefix
|
|
13
|
+
* prefix: 'uploads/',
|
|
14
|
+
* mockMode: true
|
|
16
15
|
* });
|
|
17
16
|
*
|
|
18
17
|
* const schema = {
|
|
@@ -24,6 +23,12 @@
|
|
|
24
23
|
import crypto from 'crypto'
|
|
25
24
|
import path from 'path'
|
|
26
25
|
|
|
26
|
+
const REAL_S3_NOT_IMPLEMENTED_MESSAGE = 'S3Storage real S3 mode is not implemented. The included adapter only supports mockMode: true; provide a production storage adapter or add an AWS SDK-backed implementation.'
|
|
27
|
+
|
|
28
|
+
function createRealS3NotImplementedError () {
|
|
29
|
+
return new Error(REAL_S3_NOT_IMPLEMENTED_MESSAGE)
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
export class S3Storage {
|
|
28
33
|
constructor (options = {}) {
|
|
29
34
|
this.bucket = options.bucket
|
|
@@ -35,19 +40,15 @@ export class S3Storage {
|
|
|
35
40
|
throw new Error('S3Storage requires a bucket name')
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
// Note: In a real implementation, you would initialize the AWS SDK here
|
|
39
|
-
// For this example, we'll just mock the behavior
|
|
40
43
|
this.mockMode = options.mockMode !== false // Default to mock mode
|
|
41
44
|
|
|
42
45
|
if (!this.mockMode) {
|
|
43
|
-
|
|
44
|
-
// const { S3Client } = require('@aws-sdk/client-s3');
|
|
45
|
-
// this.s3 = new S3Client({ region: this.region });
|
|
46
|
+
throw createRealS3NotImplementedError()
|
|
46
47
|
}
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
|
-
* Upload a file to S3
|
|
51
|
+
* Upload a file to mock S3 storage.
|
|
51
52
|
* @param {Object} file - File object from detector
|
|
52
53
|
* @returns {Promise<string>} URL of the uploaded file
|
|
53
54
|
*/
|
|
@@ -67,61 +68,26 @@ export class S3Storage {
|
|
|
67
68
|
return url
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
/*
|
|
72
|
-
const { PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
73
|
-
|
|
74
|
-
const command = new PutObjectCommand({
|
|
75
|
-
Bucket: this.bucket,
|
|
76
|
-
Key: key,
|
|
77
|
-
Body: file.data,
|
|
78
|
-
ContentType: file.mimetype,
|
|
79
|
-
ACL: this.acl,
|
|
80
|
-
Metadata: {
|
|
81
|
-
originalName: file.filename
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
await this.s3.send(command);
|
|
86
|
-
|
|
87
|
-
// Return public URL
|
|
88
|
-
if (this.acl === 'public-read') {
|
|
89
|
-
return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${key}`;
|
|
90
|
-
} else {
|
|
91
|
-
// For private files, you might return a signed URL
|
|
92
|
-
return await this.getSignedUrl(key);
|
|
93
|
-
}
|
|
94
|
-
*/
|
|
71
|
+
throw createRealS3NotImplementedError()
|
|
95
72
|
}
|
|
96
73
|
|
|
97
74
|
/**
|
|
98
|
-
* Delete a file from S3
|
|
75
|
+
* Delete a file from mock S3 storage.
|
|
99
76
|
* @param {string} url - URL returned by upload()
|
|
100
77
|
* @returns {Promise<void>}
|
|
101
78
|
*/
|
|
102
79
|
async delete (url) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (this.mockMode) {
|
|
106
|
-
// Mock mode - just simulate deletion
|
|
107
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
108
|
-
return
|
|
80
|
+
if (!this.mockMode) {
|
|
81
|
+
throw createRealS3NotImplementedError()
|
|
109
82
|
}
|
|
110
83
|
|
|
84
|
+
const urlObj = new URL(url)
|
|
85
|
+
|
|
111
86
|
if (urlObj.pathname.length === 0) {
|
|
112
87
|
throw new Error('Invalid S3 object URL')
|
|
113
88
|
}
|
|
114
89
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
const { DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
|
118
|
-
|
|
119
|
-
const command = new DeleteObjectCommand({
|
|
120
|
-
Bucket: this.bucket,
|
|
121
|
-
Key: key
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
await this.s3.send(command);
|
|
125
|
-
*/
|
|
90
|
+
// Mock mode - just simulate deletion
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
126
92
|
}
|
|
127
93
|
}
|
package/tests/README.md
CHANGED
|
@@ -20,7 +20,7 @@ All tests MUST use `simplified: false` to ensure JSON:API compliance:
|
|
|
20
20
|
const result = await api.resources.resourceName.method({
|
|
21
21
|
inputRecord: jsonApiDocument,
|
|
22
22
|
simplified: false,
|
|
23
|
-
returnFullRecord:
|
|
23
|
+
returnFullRecord: 'full' // Use 'no', 'minimal', or 'full' based on needs
|
|
24
24
|
});
|
|
25
25
|
```
|
|
26
26
|
|