@balena/pinejs 14.59.1 → 15.0.0-build-15-x-9f7b9bfe63741d7ae5f446e013251758d9d6ca26-1
Sign up to get free protection for your applications and to get access to all the features.
- package/.pinejs-cache.json +1 -1
- package/.versionbot/CHANGELOG.yml +110 -1
- package/CHANGELOG.md +18 -1
- package/VERSION +1 -1
- package/build/browser.ts +0 -1
- package/out/bin/abstract-sql-compiler.js +0 -0
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +0 -0
- package/out/bin/sbvr-compiler.js +0 -0
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/config-loader/config-loader.js +3 -6
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.js +4 -4
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +1 -13
- package/out/data-server/sbvr-server.js +17 -1
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.js +12 -15
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +4 -4
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +1 -12
- package/out/http-transactions/transactions.js +18 -0
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.js +12 -16
- package/out/migrator/async.js.map +1 -1
- package/out/migrator/sync.js +6 -12
- package/out/migrator/sync.js.map +1 -1
- package/out/migrator/utils.d.ts +5 -4
- package/out/migrator/utils.js +38 -20
- package/out/migrator/utils.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +18 -3
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.js +1 -1
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +1 -1
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/hooks.js +4 -5
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.js +4 -5
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.js +22 -60
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +3 -3
- package/out/sbvr-api/sbvr-utils.js +38 -21
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/uri-parser.js +4 -9
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/package.json +7 -7
- package/src/bin/abstract-sql-compiler.ts +2 -1
- package/src/bin/sbvr-compiler.ts +2 -1
- package/src/config-loader/env.ts +3 -7
- package/src/data-server/sbvr-server.js +17 -1
- package/src/database-layer/db.ts +1 -1
- package/src/express-emulator/express.js +4 -4
- package/src/http-transactions/transactions.js +18 -0
- package/src/migrator/utils.ts +40 -20
- package/src/pinejs-session-store/pinejs-session-store.ts +15 -0
- package/src/sbvr-api/odata-response.ts +1 -2
- package/src/sbvr-api/permissions.ts +17 -59
- package/src/sbvr-api/sbvr-utils.ts +30 -2
- package/src/sbvr-api/uri-parser.ts +4 -2
- package/tsconfig.json +1 -1
@@ -3,6 +3,7 @@ import { odataNameToSqlName } from '@balena/odata-to-abstract-sql';
|
|
3
3
|
// @ts-ignore
|
4
4
|
const transactionModel = require('./transaction.sbvr');
|
5
5
|
|
6
|
+
/** @type {import('../config-loader/config-loader').Config} */
|
6
7
|
export let config = {
|
7
8
|
models: [
|
8
9
|
{
|
@@ -24,6 +25,23 @@ ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT N
|
|
24
25
|
ALTER TABLE "transaction"
|
25
26
|
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;\
|
26
27
|
`,
|
28
|
+
'15.0.0-data-types': async (tx, sbvrUtils) => {
|
29
|
+
switch (sbvrUtils.db.engine) {
|
30
|
+
case 'mysql':
|
31
|
+
await tx.executeSql(`\
|
32
|
+
ALTER TABLE "lock"
|
33
|
+
MODIFY "is exclusive" BOOLEAN NOT NULL,
|
34
|
+
MODIFY "is under lock" BOOLEAN NOT NULL;`);
|
35
|
+
break;
|
36
|
+
case 'postgres':
|
37
|
+
await tx.executeSql(`\
|
38
|
+
ALTER TABLE "lock"
|
39
|
+
ALTER COLUMN "is exclusive" SET DATA TYPE BOOLEAN USING b::BOOLEAN,
|
40
|
+
ALTER COLUMN "is under lock" SET DATA TYPE BOOLEAN USING b::BOOLEAN;`);
|
41
|
+
break;
|
42
|
+
// No need to migrate for websql
|
43
|
+
}
|
44
|
+
},
|
27
45
|
},
|
28
46
|
},
|
29
47
|
],
|
package/src/migrator/utils.ts
CHANGED
@@ -12,6 +12,23 @@ import { delay } from '../sbvr-api/control-flow';
|
|
12
12
|
|
13
13
|
// tslint:disable-next-line:no-var-requires
|
14
14
|
export const modelText = require('./migrations.sbvr');
|
15
|
+
export const migrations: Migrations = {
|
16
|
+
'15.0.0-data-types': async (tx, { db }) => {
|
17
|
+
switch (db.engine) {
|
18
|
+
case 'mysql':
|
19
|
+
await tx.executeSql(`\
|
20
|
+
ALTER TABLE "migration"
|
21
|
+
MODIFY "executed migrations" JSON NOT NULL;`);
|
22
|
+
break;
|
23
|
+
case 'postgres':
|
24
|
+
await tx.executeSql(`\
|
25
|
+
ALTER TABLE "migration"
|
26
|
+
ALTER COLUMN "executed migrations" SET DATA TYPE JSONB USING b::JSONB;`);
|
27
|
+
break;
|
28
|
+
// No need to migrate for websql
|
29
|
+
}
|
30
|
+
},
|
31
|
+
};
|
15
32
|
|
16
33
|
import * as sbvrUtils from '../sbvr-api/sbvr-utils';
|
17
34
|
export enum MigrationCategories {
|
@@ -73,14 +90,14 @@ export function isSyncMigration(
|
|
73
90
|
return typeof migration === 'function' || typeof migration === 'string';
|
74
91
|
}
|
75
92
|
export function areCategorizedMigrations(
|
76
|
-
migrations: Migrations,
|
77
|
-
): migrations is CategorizedMigrations {
|
93
|
+
$migrations: Migrations,
|
94
|
+
): $migrations is CategorizedMigrations {
|
78
95
|
const containsCategories = Object.keys(MigrationCategories).some(
|
79
|
-
(key) => key in migrations,
|
96
|
+
(key) => key in $migrations,
|
80
97
|
);
|
81
98
|
if (
|
82
99
|
containsCategories &&
|
83
|
-
Object.keys(migrations).some((key) => !(key in MigrationCategories))
|
100
|
+
Object.keys($migrations).some((key) => !(key in MigrationCategories))
|
84
101
|
) {
|
85
102
|
throw new Error(
|
86
103
|
'Mixing categorized and uncategorized migrations is not supported',
|
@@ -104,31 +121,31 @@ export type MigrationStatus = {
|
|
104
121
|
};
|
105
122
|
|
106
123
|
export const getRunnableAsyncMigrations = (
|
107
|
-
migrations: Migrations,
|
124
|
+
$migrations: Migrations,
|
108
125
|
): RunnableAsyncMigrations | undefined => {
|
109
|
-
if (migrations[MigrationCategories.async]) {
|
126
|
+
if ($migrations[MigrationCategories.async]) {
|
110
127
|
if (
|
111
|
-
Object.values(migrations[MigrationCategories.async]).some(
|
128
|
+
Object.values($migrations[MigrationCategories.async]).some(
|
112
129
|
(migration) => !isAsyncMigration(migration),
|
113
130
|
) ||
|
114
|
-
typeof migrations[MigrationCategories.async] !== 'object'
|
131
|
+
typeof $migrations[MigrationCategories.async] !== 'object'
|
115
132
|
) {
|
116
133
|
throw new Error(
|
117
134
|
`All loaded async migrations need to be of type: ${MigrationCategories.async}`,
|
118
135
|
);
|
119
136
|
}
|
120
|
-
return migrations[MigrationCategories.async] as RunnableAsyncMigrations;
|
137
|
+
return $migrations[MigrationCategories.async] as RunnableAsyncMigrations;
|
121
138
|
}
|
122
139
|
};
|
123
140
|
|
124
141
|
// migration loader should either get migrations from model
|
125
142
|
// or from the filepath
|
126
143
|
export const getRunnableSyncMigrations = (
|
127
|
-
migrations: Migrations,
|
144
|
+
$migrations: Migrations,
|
128
145
|
): RunnableMigrations => {
|
129
|
-
if (areCategorizedMigrations(migrations)) {
|
146
|
+
if (areCategorizedMigrations($migrations)) {
|
130
147
|
const runnableMigrations: RunnableMigrations = {};
|
131
|
-
for (const [category, categoryMigrations] of Object.entries(migrations)) {
|
148
|
+
for (const [category, categoryMigrations] of Object.entries($migrations)) {
|
132
149
|
if (category in MigrationCategories) {
|
133
150
|
for (const [key, migration] of Object.entries(
|
134
151
|
categoryMigrations as Migrations,
|
@@ -145,16 +162,16 @@ export const getRunnableSyncMigrations = (
|
|
145
162
|
}
|
146
163
|
return runnableMigrations;
|
147
164
|
}
|
148
|
-
return migrations;
|
165
|
+
return $migrations;
|
149
166
|
};
|
150
167
|
|
151
168
|
// turns {"key1": migration, "key3": migration, "key2": migration}
|
152
169
|
// into [["key1", migration], ["key2", migration], ["key3", migration]]
|
153
170
|
export const filterAndSortPendingMigrations = (
|
154
|
-
migrations: NonNullable<RunnableMigrations | RunnableAsyncMigrations>,
|
171
|
+
$migrations: NonNullable<RunnableMigrations | RunnableAsyncMigrations>,
|
155
172
|
executedMigrations: string[],
|
156
173
|
): MigrationTuple[] =>
|
157
|
-
(_(migrations).omit(executedMigrations) as _.Object<typeof migrations>)
|
174
|
+
(_($migrations).omit(executedMigrations) as _.Object<typeof $migrations>)
|
158
175
|
.toPairs()
|
159
176
|
.sortBy(([migrationKey]) => migrationKey)
|
160
177
|
.value();
|
@@ -313,8 +330,10 @@ WHERE "migration"."model name" = ${1}`,
|
|
313
330
|
if (data == null) {
|
314
331
|
return [];
|
315
332
|
}
|
316
|
-
|
317
|
-
|
333
|
+
if (typeof data.executed_migrations === 'string') {
|
334
|
+
return JSON.parse(data.executed_migrations);
|
335
|
+
}
|
336
|
+
return data.executed_migrations;
|
318
337
|
};
|
319
338
|
|
320
339
|
export const migrationTablesExist = async (tx: Tx) => {
|
@@ -338,7 +357,7 @@ WHERE NOT EXISTS (SELECT 1 FROM "migration status" WHERE "migration key" = ${5})
|
|
338
357
|
[
|
339
358
|
migrationStatus['migration_key'],
|
340
359
|
migrationStatus['start_time'],
|
341
|
-
migrationStatus['is_backing_off']
|
360
|
+
migrationStatus['is_backing_off'],
|
342
361
|
migrationStatus['run_count'],
|
343
362
|
migrationStatus['migration_key'],
|
344
363
|
],
|
@@ -372,7 +391,7 @@ WHERE "migration status"."migration key" = ${7};`,
|
|
372
391
|
migrationStatus['migrated_row_count'],
|
373
392
|
migrationStatus['error_count'],
|
374
393
|
migrationStatus['converged_time'],
|
375
|
-
migrationStatus['is_backing_off']
|
394
|
+
migrationStatus['is_backing_off'],
|
376
395
|
migrationStatus['migration_key'],
|
377
396
|
],
|
378
397
|
);
|
@@ -409,7 +428,8 @@ LIMIT 1;`,
|
|
409
428
|
migrated_row_count: data['migrated row count'],
|
410
429
|
error_count: data['error count'],
|
411
430
|
converged_time: data['converged time'],
|
412
|
-
is_backing_off:
|
431
|
+
is_backing_off:
|
432
|
+
data['is backing off'] === true || data['is backing off'] === 1,
|
413
433
|
};
|
414
434
|
} catch (err: any) {
|
415
435
|
// we report any error here, as no error should happen at all
|
@@ -173,6 +173,21 @@ export class PinejsSessionStore extends Store {
|
|
173
173
|
ALTER TABLE "session"
|
174
174
|
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
175
175
|
`,
|
176
|
+
'15.0.0-data-types': async (tx, sbvrUtils) => {
|
177
|
+
switch (sbvrUtils.db.engine) {
|
178
|
+
case 'mysql':
|
179
|
+
await tx.executeSql(`\
|
180
|
+
ALTER TABLE "session"
|
181
|
+
MODIFY "data" JSON NOT NULL;`);
|
182
|
+
break;
|
183
|
+
case 'postgres':
|
184
|
+
await tx.executeSql(`\
|
185
|
+
ALTER TABLE "session"
|
186
|
+
ALTER COLUMN "data" SET DATA TYPE JSONB USING b::JSONB;`);
|
187
|
+
break;
|
188
|
+
// No need to migrate for websql
|
189
|
+
}
|
190
|
+
},
|
176
191
|
},
|
177
192
|
},
|
178
193
|
],
|
@@ -90,8 +90,7 @@ const getLocalFields = (table: AbstractSqlTable) => {
|
|
90
90
|
if (table.localFields == null) {
|
91
91
|
table.localFields = {};
|
92
92
|
for (const { fieldName, dataType } of table.fields) {
|
93
|
-
|
94
|
-
if (dataType !== 'ForeignKey') {
|
93
|
+
if (!['ForeignKey', 'ConceptType'].includes(dataType)) {
|
95
94
|
const odataName = sqlNameToODataName(fieldName);
|
96
95
|
table.localFields[odataName] = true;
|
97
96
|
}
|
@@ -862,9 +862,7 @@ const rewriteRelationship = memoizeWeak(
|
|
862
862
|
foundCanAccessLink = true;
|
863
863
|
}
|
864
864
|
// return a true expression to not select the relationship, which might be virtual
|
865
|
-
|
866
|
-
// is wrapped in an `or` or `and`
|
867
|
-
return ['Equals', ['Boolean', true], ['Boolean', true]];
|
865
|
+
return ['Boolean', true];
|
868
866
|
};
|
869
867
|
|
870
868
|
try {
|
@@ -1446,26 +1444,13 @@ const checkApiKey = async (
|
|
1446
1444
|
apiKey: string,
|
1447
1445
|
tx?: Tx,
|
1448
1446
|
): Promise<PermissionReq['apiKey']> => {
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
} catch (err: any) {
|
1453
|
-
console.warn('Error with API key:', err);
|
1454
|
-
// Ignore errors getting the api key and just use an empty permissions object.
|
1455
|
-
permissions = [];
|
1456
|
-
}
|
1457
|
-
let actor;
|
1458
|
-
if (permissions.length > 0) {
|
1459
|
-
actor = await getApiKeyActorId(apiKey, tx);
|
1460
|
-
}
|
1461
|
-
const resolvedApiKey: PermissionReq['apiKey'] = {
|
1447
|
+
const permissions = await getApiKeyPermissions(apiKey, tx);
|
1448
|
+
const actor = await getApiKeyActorId(apiKey, tx);
|
1449
|
+
return {
|
1462
1450
|
key: apiKey,
|
1463
1451
|
permissions,
|
1452
|
+
actor,
|
1464
1453
|
};
|
1465
|
-
if (actor != null) {
|
1466
|
-
resolvedApiKey.actor = actor;
|
1467
|
-
}
|
1468
|
-
return resolvedApiKey;
|
1469
1454
|
};
|
1470
1455
|
|
1471
1456
|
export const resolveAuthHeader = async (
|
@@ -1474,11 +1459,6 @@ export const resolveAuthHeader = async (
|
|
1474
1459
|
// TODO: Consider making tx the second argument in the next major
|
1475
1460
|
tx?: Tx,
|
1476
1461
|
): Promise<PermissionReq['apiKey']> => {
|
1477
|
-
// TODO-MAJOR: remove this check
|
1478
|
-
if (req.apiKey != null) {
|
1479
|
-
return;
|
1480
|
-
}
|
1481
|
-
|
1482
1462
|
const auth = req.header('Authorization');
|
1483
1463
|
if (!auth) {
|
1484
1464
|
return;
|
@@ -1524,11 +1504,6 @@ export const resolveApiKey = async (
|
|
1524
1504
|
// TODO: Consider making tx the second argument in the next major
|
1525
1505
|
tx?: Tx,
|
1526
1506
|
): Promise<PermissionReq['apiKey']> => {
|
1527
|
-
// TODO-MAJOR: remove this check
|
1528
|
-
if (req.apiKey != null) {
|
1529
|
-
return;
|
1530
|
-
}
|
1531
|
-
|
1532
1507
|
const apiKey =
|
1533
1508
|
req.params[paramName] ?? req.body[paramName] ?? req.query[paramName];
|
1534
1509
|
if (apiKey == null) {
|
@@ -1538,9 +1513,6 @@ export const resolveApiKey = async (
|
|
1538
1513
|
};
|
1539
1514
|
|
1540
1515
|
export const customApiKeyMiddleware = (paramName = 'apikey') => {
|
1541
|
-
if (paramName == null) {
|
1542
|
-
paramName = 'apikey';
|
1543
|
-
}
|
1544
1516
|
return async (
|
1545
1517
|
req: HookReq | Express.Request,
|
1546
1518
|
_res?: Express.Response,
|
@@ -1632,32 +1604,18 @@ const getReqPermissions = async (
|
|
1632
1604
|
req: PermissionReq,
|
1633
1605
|
odataBinds: ODataBinds = [],
|
1634
1606
|
) => {
|
1635
|
-
const
|
1636
|
-
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
1640
|
-
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
|
1646
|
-
|
1647
|
-
})(),
|
1648
|
-
(async () => {
|
1649
|
-
// TODO: Remove this extra actor ID lookup making actor non-optional and updating open-balena-api.
|
1650
|
-
if (
|
1651
|
-
req.apiKey != null &&
|
1652
|
-
req.apiKey.actor == null &&
|
1653
|
-
req.apiKey.permissions != null &&
|
1654
|
-
req.apiKey.permissions.length > 0
|
1655
|
-
) {
|
1656
|
-
const actorId = await getApiKeyActorId(req.apiKey.key);
|
1657
|
-
req.apiKey!.actor = actorId;
|
1658
|
-
}
|
1659
|
-
})(),
|
1660
|
-
]);
|
1607
|
+
const guestPermissions = await (async () => {
|
1608
|
+
if (
|
1609
|
+
guestPermissionsInitialized === false &&
|
1610
|
+
(req.user === root.user || req.user === rootRead.user)
|
1611
|
+
) {
|
1612
|
+
// In the case that guest permissions are not initialized yet and the query is being made with root permissions
|
1613
|
+
// then we need to bypass `getGuestPermissions` as it will cause an infinite loop back to here.
|
1614
|
+
// Therefore to break that loop we just ignore guest permissions.
|
1615
|
+
return [];
|
1616
|
+
}
|
1617
|
+
return await getGuestPermissions();
|
1618
|
+
})();
|
1661
1619
|
|
1662
1620
|
let actorPermissions: string[] = [];
|
1663
1621
|
const addActorPermissions = (actorId: number, perms: string[]) => {
|
@@ -123,7 +123,7 @@ export interface User extends Actor {
|
|
123
123
|
|
124
124
|
export interface ApiKey extends Actor {
|
125
125
|
key: string;
|
126
|
-
actor
|
126
|
+
actor: number;
|
127
127
|
}
|
128
128
|
|
129
129
|
export interface Response {
|
@@ -569,7 +569,10 @@ export const executeModels = async (
|
|
569
569
|
let uri = '/dev/model';
|
570
570
|
const body: AnyObject = {
|
571
571
|
is_of__vocabulary: model.vocab,
|
572
|
-
model_value:
|
572
|
+
model_value:
|
573
|
+
typeof model[modelType] === 'string'
|
574
|
+
? { value: model[modelType] }
|
575
|
+
: model[modelType],
|
573
576
|
model_type: modelType,
|
574
577
|
};
|
575
578
|
const id = result?.[0]?.id;
|
@@ -1696,6 +1699,31 @@ export const executeStandardModels = async (tx: Db.Tx): Promise<void> => {
|
|
1696
1699
|
ALTER TABLE "model"
|
1697
1700
|
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
1698
1701
|
`,
|
1702
|
+
'15.0.0-data-types': async ($tx, sbvrUtils) => {
|
1703
|
+
switch (sbvrUtils.db.engine) {
|
1704
|
+
case 'mysql':
|
1705
|
+
await $tx.executeSql(`\
|
1706
|
+
ALTER TABLE "model"
|
1707
|
+
MODIFY "model value" JSON NOT NULL;
|
1708
|
+
|
1709
|
+
UPDATE "model"
|
1710
|
+
SET "model value" = CAST('{"value":' || CAST("model value" AS CHAR) || '}' AS JSON)
|
1711
|
+
WHERE "model type" IN ('se', 'odataMetadata')
|
1712
|
+
AND CAST("model value" AS CHAR) LIKE '"%';`);
|
1713
|
+
break;
|
1714
|
+
case 'postgres':
|
1715
|
+
await $tx.executeSql(`\
|
1716
|
+
ALTER TABLE "model"
|
1717
|
+
ALTER COLUMN "model value" SET DATA TYPE JSONB USING "model value"::JSONB;
|
1718
|
+
|
1719
|
+
UPDATE "model"
|
1720
|
+
SET "model value" = CAST('{"value":' || CAST("model value" AS TEXT) || '}' AS JSON)
|
1721
|
+
WHERE "model type" IN ('se', 'odataMetadata')
|
1722
|
+
AND CAST("model value" AS TEXT) LIKE '"%';`);
|
1723
|
+
break;
|
1724
|
+
// No need to migrate for websql
|
1725
|
+
}
|
1726
|
+
},
|
1699
1727
|
},
|
1700
1728
|
});
|
1701
1729
|
await executeModels(tx, permissions.config.models);
|
@@ -144,7 +144,8 @@ export const memoizedParseOdata = (() => {
|
|
144
144
|
|
145
145
|
export const memoizedGetOData2AbstractSQL = memoizeWeak(
|
146
146
|
(abstractSqlModel: AbstractSQLCompiler.AbstractSqlModel) => {
|
147
|
-
|
147
|
+
// TODO: REMOVE THIS, it's temporary due to mismatched abstract-sql-compiler versions
|
148
|
+
return new OData2AbstractSQL(abstractSqlModel as any, undefined, {
|
148
149
|
// Use minimized aliases when not in debug mode for smaller queries
|
149
150
|
minimizeAliases: !env.DEBUG,
|
150
151
|
});
|
@@ -418,7 +419,8 @@ export const translateUri = <
|
|
418
419
|
return true;
|
419
420
|
},
|
420
421
|
});
|
421
|
-
|
422
|
+
// TODO: REMOVE THIS, it's temporary due to mismatched abstract-sql-compiler versions
|
423
|
+
request.abstractSqlQuery = abstractSqlQuery as any;
|
422
424
|
return request;
|
423
425
|
}
|
424
426
|
return request;
|