json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { getUrlPrefix } from '../../lib/querying/url-helpers.js'
|
|
2
|
+
import {
|
|
3
|
+
determineResponseStatus,
|
|
4
|
+
mapRestApiErrorToHttp
|
|
5
|
+
} from './transport-http-helpers.js'
|
|
6
|
+
|
|
7
|
+
export function extractQueryString (url = '') {
|
|
8
|
+
return String(url || '').split('?')[1] || ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildTransportRequestData ({
|
|
12
|
+
method,
|
|
13
|
+
url,
|
|
14
|
+
path,
|
|
15
|
+
headers,
|
|
16
|
+
body,
|
|
17
|
+
params,
|
|
18
|
+
query
|
|
19
|
+
}) {
|
|
20
|
+
return {
|
|
21
|
+
method,
|
|
22
|
+
url,
|
|
23
|
+
path,
|
|
24
|
+
headers,
|
|
25
|
+
body,
|
|
26
|
+
params,
|
|
27
|
+
query
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildTransportData (requestData) {
|
|
32
|
+
return {
|
|
33
|
+
request: requestData,
|
|
34
|
+
response: {
|
|
35
|
+
headers: {},
|
|
36
|
+
status: null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createConnectorContext ({
|
|
42
|
+
request,
|
|
43
|
+
reply,
|
|
44
|
+
source,
|
|
45
|
+
mountPath = '',
|
|
46
|
+
requestData,
|
|
47
|
+
createContext,
|
|
48
|
+
urlPrefixOverride
|
|
49
|
+
}) {
|
|
50
|
+
const context = createContext(request, reply, source)
|
|
51
|
+
|
|
52
|
+
if (urlPrefixOverride) {
|
|
53
|
+
context.urlPrefixOverride = urlPrefixOverride
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
context.urlPrefix = getUrlPrefix(
|
|
57
|
+
context,
|
|
58
|
+
{ vars: { transport: { mountPath } } },
|
|
59
|
+
request
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const transportData = buildTransportData(requestData)
|
|
63
|
+
context.transport = transportData
|
|
64
|
+
|
|
65
|
+
return { context, transportData }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runTransportRequestLifecycle ({ context, runHooks }) {
|
|
69
|
+
await runHooks('transport:request', context)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
rejected: !!context.rejection,
|
|
73
|
+
handled: !!context.handled
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function buildTransportRejectionBody (context) {
|
|
78
|
+
return {
|
|
79
|
+
errors: [{
|
|
80
|
+
status: String(context.rejection?.status || 500),
|
|
81
|
+
title: context.rejection?.title || 'Request Rejected',
|
|
82
|
+
detail: context.rejection?.message
|
|
83
|
+
}]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function applyTransportResponseLifecycle ({
|
|
88
|
+
context,
|
|
89
|
+
transportData,
|
|
90
|
+
status,
|
|
91
|
+
body,
|
|
92
|
+
runHooks
|
|
93
|
+
}) {
|
|
94
|
+
if (!context || !transportData) {
|
|
95
|
+
return {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
transportData.response.status = status
|
|
99
|
+
transportData.response.body = body
|
|
100
|
+
context.transport = transportData
|
|
101
|
+
|
|
102
|
+
await runHooks('transport:response', context)
|
|
103
|
+
|
|
104
|
+
return transportData.response.headers || {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function getConnectorResponseBody (result) {
|
|
108
|
+
return result && result.body !== undefined ? result.body : result
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getConnectorLocationHeader ({
|
|
112
|
+
method,
|
|
113
|
+
context,
|
|
114
|
+
routeMeta,
|
|
115
|
+
helpers,
|
|
116
|
+
mountPath = ''
|
|
117
|
+
}) {
|
|
118
|
+
if (String(method || '').toUpperCase() !== 'POST') return null
|
|
119
|
+
if (!context?.id || !routeMeta?.scopeName || !helpers?.getLocation) return null
|
|
120
|
+
|
|
121
|
+
const location = helpers.getLocation({
|
|
122
|
+
scopeName: routeMeta.scopeName,
|
|
123
|
+
id: context.id
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const baseUrl = context.urlPrefix || mountPath
|
|
127
|
+
return `${baseUrl}${location}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function executeConnectorRoute ({
|
|
131
|
+
method,
|
|
132
|
+
handler,
|
|
133
|
+
queryString,
|
|
134
|
+
headers,
|
|
135
|
+
params,
|
|
136
|
+
body,
|
|
137
|
+
context,
|
|
138
|
+
transportData,
|
|
139
|
+
routeMeta,
|
|
140
|
+
helpers,
|
|
141
|
+
mountPath = '',
|
|
142
|
+
runHooks
|
|
143
|
+
}) {
|
|
144
|
+
const result = await handler({
|
|
145
|
+
queryString,
|
|
146
|
+
headers,
|
|
147
|
+
params,
|
|
148
|
+
body,
|
|
149
|
+
context
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
const status = determineResponseStatus(method, result)
|
|
153
|
+
const transportHeaders = await applyTransportResponseLifecycle({
|
|
154
|
+
context,
|
|
155
|
+
transportData,
|
|
156
|
+
status,
|
|
157
|
+
body: result,
|
|
158
|
+
runHooks
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
status,
|
|
163
|
+
body: status === 204 ? undefined : getConnectorResponseBody(result),
|
|
164
|
+
headers: {
|
|
165
|
+
...(result?.headers || {}),
|
|
166
|
+
...transportHeaders
|
|
167
|
+
},
|
|
168
|
+
location: getConnectorLocationHeader({
|
|
169
|
+
method,
|
|
170
|
+
context,
|
|
171
|
+
routeMeta,
|
|
172
|
+
helpers,
|
|
173
|
+
mountPath
|
|
174
|
+
}),
|
|
175
|
+
result
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function handleConnectorError ({
|
|
180
|
+
error,
|
|
181
|
+
context,
|
|
182
|
+
transportData,
|
|
183
|
+
runHooks
|
|
184
|
+
}) {
|
|
185
|
+
const { status, body } = mapRestApiErrorToHttp(error)
|
|
186
|
+
const headers = await applyTransportResponseLifecycle({
|
|
187
|
+
context,
|
|
188
|
+
transportData,
|
|
189
|
+
status,
|
|
190
|
+
body,
|
|
191
|
+
runHooks
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return { status, body, headers }
|
|
195
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const JSON_API_WRITE_CONTENT_TYPES = [
|
|
2
|
+
'application/vnd.api+json',
|
|
3
|
+
'application/json'
|
|
4
|
+
]
|
|
5
|
+
|
|
6
|
+
export function isWriteMethod (method) {
|
|
7
|
+
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method || '').toUpperCase())
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getAllowedWriteContentTypes ({ allowMultipart = false } = {}) {
|
|
11
|
+
return allowMultipart
|
|
12
|
+
? [...JSON_API_WRITE_CONTENT_TYPES, 'multipart/form-data']
|
|
13
|
+
: [...JSON_API_WRITE_CONTENT_TYPES]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isAllowedWriteContentType (contentType, { allowMultipart = false } = {}) {
|
|
17
|
+
if (!contentType) return true
|
|
18
|
+
|
|
19
|
+
return getAllowedWriteContentTypes({ allowMultipart })
|
|
20
|
+
.some((allowedType) => contentType.includes(allowedType))
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getUnsupportedMediaTypeErrorBody ({ allowMultipart = false } = {}) {
|
|
24
|
+
const acceptedTypes = getAllowedWriteContentTypes({ allowMultipart }).join(' or ')
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
errors: [{
|
|
28
|
+
status: '415',
|
|
29
|
+
title: 'Unsupported Media Type',
|
|
30
|
+
detail: `Content-Type must be ${acceptedTypes}`
|
|
31
|
+
}]
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function determineResponseStatus (method, result) {
|
|
36
|
+
const upperMethod = String(method || '').toUpperCase()
|
|
37
|
+
|
|
38
|
+
if (result && typeof result.statusCode === 'number') {
|
|
39
|
+
return result.statusCode
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (upperMethod === 'POST' && result) return 201
|
|
43
|
+
if (upperMethod === 'POST' && !result) return 204
|
|
44
|
+
if (upperMethod === 'DELETE') return 204
|
|
45
|
+
if ((upperMethod === 'PUT' || upperMethod === 'PATCH') && !result) return 204
|
|
46
|
+
|
|
47
|
+
return 200
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mapRestApiErrorToHttp (error) {
|
|
51
|
+
let status = 500
|
|
52
|
+
let errors = [{
|
|
53
|
+
status: '500',
|
|
54
|
+
title: 'Internal Server Error',
|
|
55
|
+
detail: error.message
|
|
56
|
+
}]
|
|
57
|
+
|
|
58
|
+
if (error?.validation) {
|
|
59
|
+
status = 400
|
|
60
|
+
errors = error.validation.map((issue) => ({
|
|
61
|
+
status: '400',
|
|
62
|
+
title: 'Bad Request',
|
|
63
|
+
detail: issue.message,
|
|
64
|
+
...(issue.instancePath ? { source: { pointer: issue.instancePath } } : {})
|
|
65
|
+
}))
|
|
66
|
+
} else if (error.code === 'REST_API_VALIDATION') {
|
|
67
|
+
status = 422
|
|
68
|
+
if (error.details?.violations?.length) {
|
|
69
|
+
errors = error.details.violations.map((violation) => ({
|
|
70
|
+
status: '422',
|
|
71
|
+
title: 'Validation Error',
|
|
72
|
+
detail: violation.message,
|
|
73
|
+
source: { pointer: violation.field }
|
|
74
|
+
}))
|
|
75
|
+
} else {
|
|
76
|
+
errors = [{
|
|
77
|
+
status: '422',
|
|
78
|
+
title: 'Validation Error',
|
|
79
|
+
detail: error.message,
|
|
80
|
+
...(error.details ? { source: error.details } : {})
|
|
81
|
+
}]
|
|
82
|
+
}
|
|
83
|
+
} else if (error.code === 'REST_API_RESOURCE') {
|
|
84
|
+
switch (error.subtype) {
|
|
85
|
+
case 'not_found':
|
|
86
|
+
status = 404
|
|
87
|
+
errors = [{ status: '404', title: 'Not Found', detail: error.message }]
|
|
88
|
+
break
|
|
89
|
+
case 'conflict':
|
|
90
|
+
status = 409
|
|
91
|
+
errors = [{ status: '409', title: 'Conflict', detail: error.message }]
|
|
92
|
+
break
|
|
93
|
+
case 'forbidden':
|
|
94
|
+
status = 403
|
|
95
|
+
errors = [{ status: '403', title: 'Forbidden', detail: error.message }]
|
|
96
|
+
break
|
|
97
|
+
default:
|
|
98
|
+
status = 400
|
|
99
|
+
errors = [{ status: '400', title: 'Bad Request', detail: error.message }]
|
|
100
|
+
}
|
|
101
|
+
} else if (error.code === 'REST_API_PAYLOAD') {
|
|
102
|
+
status = 400
|
|
103
|
+
errors = [{
|
|
104
|
+
status: '400',
|
|
105
|
+
title: 'Bad Request',
|
|
106
|
+
detail: error.message,
|
|
107
|
+
...(error.path ? { source: { pointer: error.path } } : {})
|
|
108
|
+
}]
|
|
109
|
+
} else if (typeof error.statusCode === 'number' && error.statusCode >= 400 && error.statusCode < 500) {
|
|
110
|
+
status = error.statusCode
|
|
111
|
+
errors = [{
|
|
112
|
+
status: String(status),
|
|
113
|
+
title: status === 415 ? 'Unsupported Media Type' : 'Bad Request',
|
|
114
|
+
detail: error.message
|
|
115
|
+
}]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
status,
|
|
120
|
+
body: { errors }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { getRequestContracts } from '../../lib/querying-writing/request-contracts.js'
|
|
2
|
+
|
|
3
|
+
const STRING_OR_NUMBER_SCHEMA = {
|
|
4
|
+
anyOf: [
|
|
5
|
+
{ type: 'string' },
|
|
6
|
+
{ type: 'number' }
|
|
7
|
+
]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildResourceIdentifierSchema () {
|
|
11
|
+
return {
|
|
12
|
+
type: 'object',
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
required: ['type', 'id'],
|
|
15
|
+
properties: {
|
|
16
|
+
type: { type: 'string', minLength: 1 },
|
|
17
|
+
id: STRING_OR_NUMBER_SCHEMA
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildRelationshipValueSchema (operation) {
|
|
23
|
+
if (operation === 'patchRelationship') {
|
|
24
|
+
return {
|
|
25
|
+
anyOf: [
|
|
26
|
+
{ type: 'null' },
|
|
27
|
+
buildResourceIdentifierSchema(),
|
|
28
|
+
{
|
|
29
|
+
type: 'array',
|
|
30
|
+
items: buildResourceIdentifierSchema()
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
type: 'array',
|
|
38
|
+
items: buildResourceIdentifierSchema()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildRelationshipRouteBodySchema (operation) {
|
|
43
|
+
if (!['postRelationship', 'patchRelationship', 'deleteRelationship'].includes(operation)) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
type: 'object',
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
required: ['data'],
|
|
51
|
+
properties: {
|
|
52
|
+
data: buildRelationshipValueSchema(operation)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildTransportRouteSchema ({ routeMeta, api }) {
|
|
58
|
+
if (!routeMeta) {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (routeMeta.kind === 'relationship') {
|
|
63
|
+
const relationshipBodySchema = buildRelationshipRouteBodySchema(routeMeta.operation)
|
|
64
|
+
return relationshipBodySchema ? { body: relationshipBodySchema } : null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (routeMeta.kind !== 'resource' || !['post', 'put', 'patch'].includes(routeMeta.operation)) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const scope = api.resources?.[routeMeta.scopeName]
|
|
72
|
+
const schemaInfo = scope?.vars?.schemaInfo
|
|
73
|
+
if (!schemaInfo?.schemaInstance) {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const requestContracts = getRequestContracts({
|
|
78
|
+
scopeName: routeMeta.scopeName,
|
|
79
|
+
schemaInfo,
|
|
80
|
+
includeDepthLimit: scope?.vars?.includeDepthLimit,
|
|
81
|
+
sortableFields: scope?.vars?.sortableFields
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const contract = requestContracts[routeMeta.operation]
|
|
85
|
+
if (!contract?.schema) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
body: contract.schema.toJsonSchema({ mode: contract.mode, additionalProperties: false })
|
|
91
|
+
}
|
|
92
|
+
}
|