@contentstack/cli-cm-import 1.14.3 → 1.15.1
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 +1 -1
- package/lib/import/module-importer.js +1 -0
- package/lib/import/modules/entries.d.ts +3 -1
- package/lib/import/modules/entries.js +29 -6
- package/lib/import/modules/marketplace-apps.js +34 -24
- package/lib/import/modules-js/marketplace-apps.js +1 -1
- package/lib/types/default-config.d.ts +1 -0
- package/lib/types/import-config.d.ts +1 -0
- package/lib/utils/asset-helper.js +5 -3
- package/lib/utils/content-type-helper.js +5 -0
- package/lib/utils/entries-helper.js +38 -0
- package/lib/utils/interactive.js +1 -1
- package/lib/utils/marketplace-app-helper.js +8 -3
- package/oclif.manifest.json +2 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ $ npm install -g @contentstack/cli-cm-import
|
|
|
47
47
|
$ csdx COMMAND
|
|
48
48
|
running command...
|
|
49
49
|
$ csdx (--version)
|
|
50
|
-
@contentstack/cli-cm-import/1.
|
|
50
|
+
@contentstack/cli-cm-import/1.15.1 linux-x64 node-v18.20.1
|
|
51
51
|
$ csdx --help [COMMAND]
|
|
52
52
|
USAGE
|
|
53
53
|
$ csdx COMMAND
|
|
@@ -99,6 +99,7 @@ class ModuleImporter {
|
|
|
99
99
|
const basePath = (0, path_1.resolve)(this.importConfig.backupDir, 'logs', 'audit');
|
|
100
100
|
const auditConfig = this.importConfig.auditConfig;
|
|
101
101
|
auditConfig.config.basePath = basePath;
|
|
102
|
+
auditConfig.config.branch = this.importConfig.branchName;
|
|
102
103
|
try {
|
|
103
104
|
const args = [
|
|
104
105
|
'--data-dir',
|
|
@@ -36,6 +36,8 @@ export default class EntriesImport extends BaseClass {
|
|
|
36
36
|
private autoCreatedEntries;
|
|
37
37
|
private taxonomiesPath;
|
|
38
38
|
taxonomies: Record<string, unknown>;
|
|
39
|
+
rteCTs: any;
|
|
40
|
+
rteCTsWithRef: any;
|
|
39
41
|
constructor({ importConfig, stackAPIClient }: ModuleClassParams);
|
|
40
42
|
start(): Promise<any>;
|
|
41
43
|
disableMandatoryCTReferences(): Promise<void>;
|
|
@@ -63,7 +65,7 @@ export default class EntriesImport extends BaseClass {
|
|
|
63
65
|
cTUid: string;
|
|
64
66
|
locale: string;
|
|
65
67
|
}): Promise<void>;
|
|
66
|
-
replaceEntriesHandler({ apiParams, element: entry }: {
|
|
68
|
+
replaceEntriesHandler({ apiParams, element: entry, }: {
|
|
67
69
|
apiParams: ApiOptions;
|
|
68
70
|
element: Record<string, string>;
|
|
69
71
|
isLastRequest: boolean;
|
|
@@ -37,6 +37,8 @@ class EntriesImport extends base_class_1.default {
|
|
|
37
37
|
this.envs = {};
|
|
38
38
|
this.autoCreatedEntries = [];
|
|
39
39
|
this.failedEntries = [];
|
|
40
|
+
this.rteCTs = [];
|
|
41
|
+
this.rteCTsWithRef = [];
|
|
40
42
|
}
|
|
41
43
|
async start() {
|
|
42
44
|
var _a;
|
|
@@ -150,6 +152,8 @@ class EntriesImport extends base_class_1.default {
|
|
|
150
152
|
references: false,
|
|
151
153
|
jsonRte: false,
|
|
152
154
|
jsonRteEmbeddedEntries: false,
|
|
155
|
+
rte: false,
|
|
156
|
+
rteEmbeddedEntries: false,
|
|
153
157
|
};
|
|
154
158
|
(0, utils_1.suppressSchemaReference)(contentType.schema, flag);
|
|
155
159
|
if (flag.references) {
|
|
@@ -164,6 +168,15 @@ class EntriesImport extends base_class_1.default {
|
|
|
164
168
|
}
|
|
165
169
|
}
|
|
166
170
|
}
|
|
171
|
+
if (flag.rte) {
|
|
172
|
+
this.rteCTs.push(contentType.uid);
|
|
173
|
+
if (flag.rteEmbeddedEntries) {
|
|
174
|
+
this.rteCTsWithRef.push(contentType.uid);
|
|
175
|
+
if (this.refCTs.indexOf(contentType.uid) === -1) {
|
|
176
|
+
this.refCTs.push(contentType.uid);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
167
180
|
// Check if suppress modified flag
|
|
168
181
|
if (flag.suppressed) {
|
|
169
182
|
this.modifiedCTs.push((0, lodash_1.find)(this.cTs, { uid: contentType.uid }));
|
|
@@ -318,6 +331,12 @@ class EntriesImport extends base_class_1.default {
|
|
|
318
331
|
if (this.jsonRteCTsWithRef.indexOf(cTUid) > -1) {
|
|
319
332
|
entry = (0, utils_1.removeEntryRefsFromJSONRTE)(entry, contentType.schema);
|
|
320
333
|
}
|
|
334
|
+
if (this.rteCTs.indexOf(cTUid) > -1) {
|
|
335
|
+
entry = (0, utils_1.removeEntryRefsFromJSONRTE)(entry, contentType.schema);
|
|
336
|
+
}
|
|
337
|
+
if (this.rteCTsWithRef.indexOf(cTUid) > -1) {
|
|
338
|
+
entry = (0, utils_1.removeEntryRefsFromJSONRTE)(entry, contentType.schema);
|
|
339
|
+
}
|
|
321
340
|
//will remove term if term doesn't exists in taxonomy
|
|
322
341
|
(0, utils_1.lookUpTerms)(contentType === null || contentType === void 0 ? void 0 : contentType.schema, entry, this.taxonomies, this.importConfig);
|
|
323
342
|
// will replace all old asset uid/urls with new ones
|
|
@@ -410,7 +429,7 @@ class EntriesImport extends base_class_1.default {
|
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
431
|
}
|
|
413
|
-
async replaceEntriesHandler({ apiParams, element: entry }) {
|
|
432
|
+
async replaceEntriesHandler({ apiParams, element: entry, }) {
|
|
414
433
|
const { additionalInfo: { cTUid, locale } = {} } = apiParams;
|
|
415
434
|
return new Promise(async (resolve, reject) => {
|
|
416
435
|
const { items: [entryInStack] = [] } = (await this.stack
|
|
@@ -536,7 +555,7 @@ class EntriesImport extends base_class_1.default {
|
|
|
536
555
|
// Removing temp values
|
|
537
556
|
delete entry.sourceEntryFilePath;
|
|
538
557
|
delete entry.entryOldUid;
|
|
539
|
-
if (this.jsonRteCTs.indexOf(cTUid) > -1) {
|
|
558
|
+
if (this.jsonRteCTs.indexOf(cTUid) > -1 || this.rteCTs.indexOf(cTUid) > -1) {
|
|
540
559
|
// the entries stored in eSuccessFilePath, have the same uids as the entries from source data
|
|
541
560
|
entry = (0, utils_1.restoreJsonRteEntryRefs)(entry, sourceEntry, contentType.schema, {
|
|
542
561
|
uidMapper: this.entriesUidMapper,
|
|
@@ -544,6 +563,10 @@ class EntriesImport extends base_class_1.default {
|
|
|
544
563
|
mappedAssetUrls: this.assetUrlMapper,
|
|
545
564
|
});
|
|
546
565
|
}
|
|
566
|
+
entry = (0, utils_1.lookupAssets)({
|
|
567
|
+
content_type: contentType,
|
|
568
|
+
entry: entry,
|
|
569
|
+
}, this.assetUidMapper, this.assetUrlMapper, path.join(this.entriesPath, cTUid), this.installedExtensions);
|
|
547
570
|
entry = (0, utils_1.lookupEntries)({
|
|
548
571
|
content_type: contentType,
|
|
549
572
|
entry,
|
|
@@ -687,11 +710,11 @@ class EntriesImport extends base_class_1.default {
|
|
|
687
710
|
return Promise.resolve();
|
|
688
711
|
}
|
|
689
712
|
// log(this.importConfig, `Starting publish entries for ${cTUid} in locale ${locale}`, 'info');
|
|
690
|
-
const onSuccess = ({ response, apiData: { environments, entryUid }, additionalInfo }) => {
|
|
691
|
-
(0, utils_1.log)(this.importConfig, `Published entry: '${entryUid}' of
|
|
713
|
+
const onSuccess = ({ response, apiData: { environments, entryUid, locales }, additionalInfo }) => {
|
|
714
|
+
(0, utils_1.log)(this.importConfig, `Published the entry: '${entryUid}' of Content Type '${cTUid}' and Locale '${locale}' in Environments '${environments === null || environments === void 0 ? void 0 : environments.join(',')}' and Locales '${locales === null || locales === void 0 ? void 0 : locales.join(',')}'`, 'info');
|
|
692
715
|
};
|
|
693
|
-
const onReject = ({ error, apiData, additionalInfo }) => {
|
|
694
|
-
(0, utils_1.log)(this.importConfig,
|
|
716
|
+
const onReject = ({ error, apiData: { environments, entryUid, locales }, additionalInfo }) => {
|
|
717
|
+
(0, utils_1.log)(this.importConfig, `Failed to publish: '${entryUid}' entry of Content Type '${cTUid}' and Locale '${locale}' in Environments '${environments === null || environments === void 0 ? void 0 : environments.join(',')}' and Locales '${locales === null || locales === void 0 ? void 0 : locales.join(',')}'`, 'error');
|
|
695
718
|
(0, utils_1.log)(this.importConfig, (0, utils_1.formatError)(error), 'error');
|
|
696
719
|
};
|
|
697
720
|
for (const index in indexer) {
|
|
@@ -163,31 +163,37 @@ class ImportMarketplaceApps {
|
|
|
163
163
|
if ((0, isEmpty_1.default)(privateApps)) {
|
|
164
164
|
return Promise.resolve();
|
|
165
165
|
}
|
|
166
|
-
await (0, utils_1.getConfirmationToCreateApps)(privateApps, this.importConfig);
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
let canCreatePrivateApp = await (0, utils_1.getConfirmationToCreateApps)(privateApps, this.importConfig);
|
|
167
|
+
this.importConfig.canCreatePrivateApp = canCreatePrivateApp;
|
|
168
|
+
if (canCreatePrivateApp) {
|
|
169
|
+
(0, utils_1.log)(this.importConfig, 'Starting developer hub private apps re-creation', 'success');
|
|
170
|
+
for (let app of privateApps) {
|
|
171
|
+
if (this.importConfig.skipPrivateAppRecreationIfExist && (await this.isPrivateAppExistInDeveloperHub(app))) {
|
|
172
|
+
// NOTE Found app already exist in the same org
|
|
173
|
+
this.appUidMapping[app.uid] = app.uid;
|
|
174
|
+
cli_utilities_1.cliux.print(`App '${app.manifest.name}' already exist. skipping app recreation.!`, { color: 'yellow' });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
// NOTE keys can be passed to install new app in the developer hub
|
|
178
|
+
const validKeys = [
|
|
179
|
+
'uid',
|
|
180
|
+
'name',
|
|
181
|
+
'icon',
|
|
182
|
+
'oauth',
|
|
183
|
+
'webhook',
|
|
184
|
+
'visibility',
|
|
185
|
+
'target_type',
|
|
186
|
+
'description',
|
|
187
|
+
'ui_location',
|
|
188
|
+
'framework_version',
|
|
189
|
+
];
|
|
190
|
+
const manifest = (0, pick_1.default)(app.manifest, validKeys);
|
|
191
|
+
this.appOriginalName = manifest.name;
|
|
192
|
+
await this.createPrivateApp(manifest);
|
|
174
193
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
'name',
|
|
179
|
-
'icon',
|
|
180
|
-
'oauth',
|
|
181
|
-
'webhook',
|
|
182
|
-
'visibility',
|
|
183
|
-
'target_type',
|
|
184
|
-
'description',
|
|
185
|
-
'ui_location',
|
|
186
|
-
'framework_version',
|
|
187
|
-
];
|
|
188
|
-
const manifest = (0, pick_1.default)(app.manifest, validKeys);
|
|
189
|
-
this.appOriginalName = manifest.name;
|
|
190
|
-
await this.createPrivateApp(manifest);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
(0, utils_1.log)(this.importConfig, 'Skipping private apps creation on Developer Hub...', 'success');
|
|
191
197
|
}
|
|
192
198
|
this.appOriginalName = undefined;
|
|
193
199
|
}
|
|
@@ -332,6 +338,10 @@ class ImportMarketplaceApps {
|
|
|
332
338
|
const currentStackApp = (0, find_1.default)(this.installedApps, { manifest: { uid: (_a = app === null || app === void 0 ? void 0 : app.manifest) === null || _a === void 0 ? void 0 : _a.uid } });
|
|
333
339
|
if (!currentStackApp) {
|
|
334
340
|
// NOTE install new app
|
|
341
|
+
if (app.manifest.visibility === 'private' && !this.importConfig.canCreatePrivateApp) {
|
|
342
|
+
(0, utils_1.log)(this.importConfig, `Skipping the installation of the private app ${app.manifest.name}...`, 'info');
|
|
343
|
+
return Promise.resolve();
|
|
344
|
+
}
|
|
335
345
|
const installation = await this.installApp(this.importConfig,
|
|
336
346
|
// NOTE if it's private app it should get uid from mapper else will use manifest uid
|
|
337
347
|
this.appUidMapping[app.manifest.uid] || app.manifest.uid);
|
|
@@ -85,7 +85,7 @@ module.exports = class ImportMarketplaceApps {
|
|
|
85
85
|
return "Encryption key can't be empty.";
|
|
86
86
|
return true;
|
|
87
87
|
},
|
|
88
|
-
message: 'Enter
|
|
88
|
+
message: 'Enter Marketplace app configurations encryption key',
|
|
89
89
|
});
|
|
90
90
|
try {
|
|
91
91
|
appConfig = !_.isEmpty(appConfig.configuration) ? appConfig.configuration : appConfig.server_configuration;
|
|
@@ -235,9 +235,11 @@ const lookupAssets = function (data, mappedAssetUids, mappedAssetUrls, assetUidM
|
|
|
235
235
|
assetUrls.forEach(function (assetUrl) {
|
|
236
236
|
let mappedAssetUrl = mappedAssetUrls[assetUrl];
|
|
237
237
|
if (typeof mappedAssetUrl !== 'undefined') {
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
238
|
+
//NOTE - This code was added to resolve the SRE issue but once the code was merged Assets URLs in JSON RTE started breaking
|
|
239
|
+
// const sanitizedUrl = escapeRegExp(assetUrl).replace(/\.\./g, '\\$&');
|
|
240
|
+
// const escapedMappedUrl = escapeRegExp(mappedAssetUrl).replace(/\.\./g, '\\$&');
|
|
241
|
+
// entry = entry.replace(new RegExp(sanitizedUrl, 'img'), escapedMappedUrl);
|
|
242
|
+
entry = entry.replace(new RegExp(assetUrl, 'img'), mappedAssetUrl);
|
|
241
243
|
matchedUrls.push(mappedAssetUrl);
|
|
242
244
|
}
|
|
243
245
|
else {
|
|
@@ -68,6 +68,11 @@ const suppressSchemaReference = function (schema, flag) {
|
|
|
68
68
|
if (schema[i].field_metadata.embed_entry === true)
|
|
69
69
|
flag.jsonRteEmbeddedEntries = true;
|
|
70
70
|
}
|
|
71
|
+
else if (schema[i].data_type === 'text' && schema[i].field_metadata.rich_text_type) {
|
|
72
|
+
flag.rte = true;
|
|
73
|
+
if (schema[i].field_metadata.embed_entry === true)
|
|
74
|
+
flag.rteEmbeddedEntries = true;
|
|
75
|
+
}
|
|
71
76
|
if ((schema[i].hasOwnProperty('mandatory') && schema[i].mandatory) ||
|
|
72
77
|
(schema[i].hasOwnProperty('unique') && schema[i].unique)) {
|
|
73
78
|
if (schema[i].uid !== 'title') {
|
|
@@ -418,6 +418,21 @@ const removeEntryRefsFromJSONRTE = (entry, ctSchema = []) => {
|
|
|
418
418
|
}
|
|
419
419
|
break;
|
|
420
420
|
}
|
|
421
|
+
case 'text': {
|
|
422
|
+
if (entry[element.uid] && element.field_metadata.rich_text_type) {
|
|
423
|
+
if (element.multiple) {
|
|
424
|
+
let rteContent = [];
|
|
425
|
+
for (let i = 0; i < entry[element.uid].length; i++) {
|
|
426
|
+
rteContent.push('<p></p>');
|
|
427
|
+
}
|
|
428
|
+
entry[element.uid] = rteContent;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
entry[element.uid] = '<p></p>';
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
421
436
|
}
|
|
422
437
|
}
|
|
423
438
|
return entry;
|
|
@@ -525,11 +540,34 @@ const restoreJsonRteEntryRefs = (entry, sourceStackEntry, ctSchema = [], { uidMa
|
|
|
525
540
|
}
|
|
526
541
|
break;
|
|
527
542
|
}
|
|
543
|
+
case 'text': {
|
|
544
|
+
if (entry[element.uid] && element.field_metadata.rich_text_type) {
|
|
545
|
+
entry[element.uid] = sourceStackEntry[element.uid];
|
|
546
|
+
const matches = Object.keys(uidMapper).filter((uid) => {
|
|
547
|
+
if (sourceStackEntry[element.uid].indexOf(uid) !== -1)
|
|
548
|
+
return uid;
|
|
549
|
+
});
|
|
550
|
+
if (element.multiple && Array.isArray(entry[element.uid])) {
|
|
551
|
+
for (let i = 0; i < matches.length; i++) {
|
|
552
|
+
entry[element.uid] = entry[element.uid].map((el) => updateUids(el, matches[i], uidMapper));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
for (let i = 0; i < matches.length; i++) {
|
|
557
|
+
entry[element.uid] = updateUids(entry[element.uid], matches[i], uidMapper);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
528
563
|
}
|
|
529
564
|
}
|
|
530
565
|
return entry;
|
|
531
566
|
};
|
|
532
567
|
exports.restoreJsonRteEntryRefs = restoreJsonRteEntryRefs;
|
|
568
|
+
function updateUids(str, match, uidMapper) {
|
|
569
|
+
return str.replace(new RegExp(match, 'g'), (match) => uidMapper[match]);
|
|
570
|
+
}
|
|
533
571
|
function setDirtyTrue(jsonRteChild) {
|
|
534
572
|
// also removing uids in this function
|
|
535
573
|
if (jsonRteChild.type) {
|
package/lib/utils/interactive.js
CHANGED
|
@@ -35,7 +35,7 @@ const askEncryptionKey = async (defaultValue) => {
|
|
|
35
35
|
return "Encryption key can't be empty.";
|
|
36
36
|
return true;
|
|
37
37
|
},
|
|
38
|
-
message: 'Enter
|
|
38
|
+
message: 'Enter Marketplace app configurations encryption key',
|
|
39
39
|
});
|
|
40
40
|
};
|
|
41
41
|
exports.askEncryptionKey = askEncryptionKey;
|
|
@@ -60,10 +60,15 @@ const getConfirmationToCreateApps = async (privateApps, config) => {
|
|
|
60
60
|
if (!config.forceStopMarketplaceAppsPrompt) {
|
|
61
61
|
if (!(await cli_utilities_1.cliux.confirm(chalk_1.default.yellow(`WARNING!!! The listed apps are private apps that are not available in the destination stack: \n\n${(0, map_1.default)(privateApps, ({ manifest: { name } }, index) => `${String(index + 1)}) ${name}`).join('\n')}\n\nWould you like to re-create the private app and then proceed with the installation? (y/n)`)))) {
|
|
62
62
|
if (await cli_utilities_1.cliux.confirm(chalk_1.default.yellow(`\nWARNING!!! Canceling the app re-creation may break the content type and entry import. Would you like to proceed without re-create the private app? (y/n)`))) {
|
|
63
|
-
return Promise.resolve(
|
|
63
|
+
return Promise.resolve(false);
|
|
64
64
|
}
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
else {
|
|
66
|
+
if (await cli_utilities_1.cliux.confirm(chalk_1.default.yellow('\nWould you like to re-create the private app and then proceed with the installation? (y/n)'))) {
|
|
67
|
+
return Promise.resolve(true);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return Promise.resolve(false);
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
74
|
}
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.
|
|
2
|
+
"version": "1.15.1",
|
|
3
3
|
"commands": {
|
|
4
4
|
"cm:stacks:import": {
|
|
5
5
|
"id": "cm:stacks:import",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"aliases": [
|
|
13
13
|
"cm:import"
|
|
14
14
|
],
|
|
15
|
+
"hiddenAliases": [],
|
|
15
16
|
"examples": [
|
|
16
17
|
"csdx cm:stacks:import --stack-api-key <stack_api_key> --data-dir <path/of/export/destination/dir>",
|
|
17
18
|
"csdx cm:stacks:import --config <path/of/config/dir>",
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contentstack/cli-cm-import",
|
|
3
3
|
"description": "Contentstack CLI plugin to import content into stack",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.15.1",
|
|
5
5
|
"author": "Contentstack",
|
|
6
6
|
"bugs": "https://github.com/contentstack/cli/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@contentstack/cli-audit": "~1.5.
|
|
8
|
+
"@contentstack/cli-audit": "~1.5.3",
|
|
9
9
|
"@contentstack/cli-command": "~1.2.16",
|
|
10
10
|
"@contentstack/cli-utilities": "~1.6.0",
|
|
11
11
|
"@contentstack/management": "~1.15.3",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@types/mocha": "^8.2.2",
|
|
35
35
|
"@types/node": "^14.14.32",
|
|
36
36
|
"@types/sinon": "^10.0.2",
|
|
37
|
-
"@types/tar": "^
|
|
37
|
+
"@types/tar": "^6.1.3",
|
|
38
38
|
"@types/uuid": "^9.0.7",
|
|
39
39
|
"@typescript-eslint/eslint-plugin": "^5.48.2",
|
|
40
40
|
"chai": "^4.2.0",
|
|
@@ -99,4 +99,4 @@
|
|
|
99
99
|
}
|
|
100
100
|
},
|
|
101
101
|
"repository": "https://github.com/contentstack/cli"
|
|
102
|
-
}
|
|
102
|
+
}
|