@cap-js-community/common 0.1.0

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.
@@ -0,0 +1,588 @@
1
+ "use strict";
2
+
3
+ const cds = require("@sap/cds");
4
+ const path = require("path");
5
+ const fs = require("fs");
6
+ const crypto = require("crypto");
7
+
8
+ const COMPONENT_NAME = "migrationCheck";
9
+ const STRING_DEFAULT_LENGTH = 5000;
10
+
11
+ const Checks = [releasedEntityCheck, newEntityCheck, uniqueIndexCheck];
12
+ const Messages = {
13
+ ReleasedEntityCannotBeRemoved: "A released entity cannot be removed",
14
+ ReleasedEntityDraftEnablementCannotBeChanged: "The draft enablement state of a released entity cannot be changed",
15
+ ReleasedElementCannotBeRemoved: "A released element cannot be removed",
16
+ ReleasedElementKeyCannotBeChanged: "The key of a released element cannot be changed",
17
+ ReleasedElementManagedUnmanagedCannotBeChanged: "The managed/unmanaged state of a released element cannot be changed",
18
+ ReleasedElementVirtualCannotBeChanged: "The virtual state of a released element cannot be changed",
19
+ ReleasedElementLocalizationCannotBeChanged: "The localization state of a released element cannot be changed",
20
+ ReleasedElementNullableCannotBeChanged: "A released element cannot be changed to not-nullable",
21
+ ReleasedElementTypeCannotBeChanged: "The data type of a released element cannot be changed",
22
+ ReleasedElementTypeCannotBeShortened: "The data type of a released element cannot be shortened",
23
+ ReleasedElementScalePrecisionCannotBeLower: "The scale or precision of a released element cannot be reduced",
24
+ ReleasedElementTargetCannotBeChanged: "The target of a released element cannot be changed",
25
+ ReleasedElementCardinalityCannotBeChanged: "The cardinality of a released element cannot be changed",
26
+ ReleasedElementOnConditionCannotBeChanged: "The ON condition of a released element cannot be changed",
27
+ ReleasedElementKeysConditionCannotBeChanged: "The keys condition of a released element cannot be changed",
28
+ ReleasedEntityIndexChangeIsNotAllowed: "Changes to the index of a released entity are not allowed",
29
+ ReleasedEntityIndexChangeIsNotWhitelisted: "Changes to the index of a released entity must be whitelisted",
30
+ ReleasedElementTypeExtensionIsNotWhitelisted: "Extending the type of a released element requires whitelisting",
31
+ ReleasedElementScalePrecisionExtensionIsNotWhitelisted:
32
+ "Extending the scale or precision of a released element requires whitelisting",
33
+
34
+ NewEntityIsNotWhitelisted: "The new entity is not whitelisted",
35
+ NewEntityElementIsNotWhitelisted: "The new entity element is not whitelisted",
36
+ NewEntityElementNotNullableDefault: "A new entity element must have a default value if it is not nullable",
37
+ NewEntityIndexIsNotWhitelisted: "The new entity index is not whitelisted",
38
+ };
39
+ const MessagesCodes = Object.keys(Messages).reduce((codes, key) => {
40
+ codes[key] = key;
41
+ return codes;
42
+ }, {});
43
+
44
+ class MigrationCheck {
45
+ constructor(options) {
46
+ this.log = cds.log(COMPONENT_NAME);
47
+ this.options = {
48
+ ...(cds.env.migrationCheck || {}),
49
+ ...options,
50
+ };
51
+ const basePath = path.join(process.cwd(), this.options.baseDir);
52
+ this.paths = {
53
+ basePath,
54
+ buildNodePath: path.join(process.cwd(), "./gen/srv/srv/csn.json"),
55
+ buildJavaPath: path.join(process.cwd(), "./srv/src/main/resources/edmx/csn.json"),
56
+ buildCustomPath: this.options.buildPath,
57
+ prodPath: path.join(basePath, "./csn-prod.json"),
58
+ prodHashPath: path.join(basePath, "./csn-prod-hash.json"),
59
+ prodWhitelistPath: path.join(basePath, "./migration-extension-whitelist.json"),
60
+ prodWhitelistHashPath: path.join(basePath, "./migration-extension-whitelist-hash.json"),
61
+ prodFreeze: path.join(basePath, "./csn-prod.freeze"),
62
+ };
63
+ this.setup();
64
+ }
65
+
66
+ setup() {
67
+ this.buildPath();
68
+ this.freeze();
69
+ }
70
+
71
+ buildPath() {
72
+ this.paths.buildPath = this.paths.buildCustomPath;
73
+ if (!this.paths.buildPath && fs.existsSync(this.paths.buildNodePath)) {
74
+ this.paths.buildPath = this.paths.buildNodePath;
75
+ }
76
+ if (!this.paths.buildPath && fs.existsSync(this.paths.buildJavaPath)) {
77
+ this.paths.buildPath = this.paths.buildJavaPath;
78
+ }
79
+ this.paths.buildPath ??= this.paths.buildNodePath;
80
+ return this.paths.buildPath;
81
+ }
82
+
83
+ freeze() {
84
+ return this.options.freeze || fs.existsSync(this.paths.prodFreeze);
85
+ }
86
+
87
+ check(admin) {
88
+ let checkMessages = [];
89
+ let csnBuild;
90
+ try {
91
+ csnBuild = JSON.parse(fs.readFileSync(this.buildPath()));
92
+ } catch (err) {
93
+ if (err instanceof SyntaxError) {
94
+ throw err;
95
+ }
96
+ return {
97
+ success: false,
98
+ messages: [
99
+ {
100
+ code: "NoValidBuildCSN",
101
+ text: `No valid build CSN found for CSN build path. Migration check cannot be started. Execute 'cds build --production' before`,
102
+ severity: "error",
103
+ },
104
+ ],
105
+ };
106
+ }
107
+ let csnProd;
108
+ let csnProdChecksum;
109
+ try {
110
+ csnProd = JSON.parse(fs.readFileSync(this.paths.prodPath));
111
+ } catch (err) {
112
+ if (err instanceof SyntaxError) {
113
+ throw err;
114
+ }
115
+ return {
116
+ success: true,
117
+ messages: [
118
+ {
119
+ code: "NoProdCSNMigrationSkipped",
120
+ text: `No valid production CSN found for path '${this.paths.prodPath}'. Execute 'cdsmc -u' to update production CSN. Migration check is skipped.`,
121
+ severity: "info",
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ try {
127
+ csnProdChecksum = JSON.parse(fs.readFileSync(this.paths.prodHashPath)).checksum;
128
+ } catch (err) {
129
+ if (err instanceof SyntaxError) {
130
+ throw err;
131
+ }
132
+ }
133
+ const prodData = fs.readFileSync(this.paths.prodPath);
134
+ const prodDataHash = hash(prodData);
135
+ if (prodDataHash !== csnProdChecksum) {
136
+ return {
137
+ success: false,
138
+ messages: [
139
+ {
140
+ code: "HashMismatch",
141
+ text: `Hash mismatch. Production CSN is protected: ${prodDataHash} <> ${csnProdChecksum}`,
142
+ severity: "error",
143
+ },
144
+ ],
145
+ };
146
+ }
147
+
148
+ let whitelist = { definitions: {} };
149
+ try {
150
+ whitelist = JSON.parse(fs.readFileSync(this.paths.prodWhitelistPath));
151
+ } catch (err) {
152
+ if (err instanceof SyntaxError) {
153
+ throw err;
154
+ }
155
+ }
156
+ let whitelistChecksum;
157
+ try {
158
+ whitelistChecksum = JSON.parse(fs.readFileSync(this.paths.prodWhitelistHashPath)).checksum;
159
+ } catch (err) {
160
+ if (err instanceof SyntaxError) {
161
+ throw err;
162
+ }
163
+ }
164
+ if (this.freeze()) {
165
+ if (whitelistChecksum) {
166
+ const whiteListData = fs.readFileSync(this.paths.prodWhitelistPath);
167
+ const whiteListDataHash = hash(whiteListData);
168
+ if (whiteListDataHash !== whitelistChecksum) {
169
+ return {
170
+ success: false,
171
+ messages: [
172
+ {
173
+ code: "HashMismatch",
174
+ text: `Hash mismatch. Production CSN Whitelist is protected (Persistence Freeze): ${whiteListDataHash} <> ${whitelistChecksum}`,
175
+ severity: "error",
176
+ },
177
+ ],
178
+ };
179
+ }
180
+ } else {
181
+ checkMessages.push({
182
+ code: "PersistenceFreezeCheckSkipped",
183
+ text: `No Production CSN Whitelist Checksum file found for path '${this.paths.prodWhitelistHashPath}'. Persistence Freeze check skipped.`,
184
+ severity: "info",
185
+ });
186
+ }
187
+ }
188
+ const messages = Checks.reduce((messages, check) => {
189
+ messages.push(...check(csnBuild, csnProd, whitelist, this.options));
190
+ return messages;
191
+ }, []);
192
+ messages.push(...checkMessages);
193
+ const result = {
194
+ success: true,
195
+ messages,
196
+ adminHash: null,
197
+ };
198
+ if (messages.length > 0) {
199
+ const messageHash = hash(JSON.stringify(messages));
200
+ if (admin) {
201
+ result.adminHash = messageHash;
202
+ }
203
+ if (this.options.adminHash && this.options.adminHash === messageHash) {
204
+ for (const message of result.messages) {
205
+ message.severity = message.severity === "error" ? "warning" : message.severity;
206
+ }
207
+ messages.push({
208
+ code: "AcceptedByAdmin",
209
+ text: "Migration check errors accepted by admin",
210
+ severity: "info",
211
+ });
212
+ result.success = true;
213
+ } else {
214
+ result.success = false;
215
+ }
216
+ }
217
+ return result;
218
+ }
219
+
220
+ update(admin) {
221
+ if (fs.existsSync(this.paths.prodPath)) {
222
+ const checkResult = this.check(admin);
223
+ if (!checkResult?.success) {
224
+ return checkResult;
225
+ }
226
+ }
227
+ try {
228
+ JSON.parse(fs.readFileSync(this.buildPath()));
229
+ } catch (err) {
230
+ if (err instanceof SyntaxError) {
231
+ throw err;
232
+ }
233
+ return {
234
+ success: false,
235
+ messages: [
236
+ {
237
+ code: "NoValidBuildCSN",
238
+ text: `No valid build CSN found for CSN build path. Migration update cannot be started. Execute 'cds build --production' before`,
239
+ severity: "error",
240
+ },
241
+ ],
242
+ };
243
+ }
244
+ fs.mkdirSync(this.paths.basePath, { recursive: true });
245
+ fs.copyFileSync(this.buildPath(), this.paths.prodPath);
246
+ let description = new Date().toISOString();
247
+ if (this.options.label) {
248
+ description = `${this.options.label} (${description})`;
249
+ }
250
+ const prodData = fs.readFileSync(this.paths.prodPath);
251
+ fs.writeFileSync(
252
+ this.paths.prodHashPath,
253
+ JSON.stringify(
254
+ {
255
+ description,
256
+ checksum: hash(prodData),
257
+ "checksum-sha1": hash(prodData, "sha1"),
258
+ },
259
+ null,
260
+ 2,
261
+ ) + "\n",
262
+ );
263
+ if (!fs.existsSync(this.paths.prodWhitelistPath) || !this.options.keep) {
264
+ fs.writeFileSync(
265
+ this.paths.prodWhitelistPath,
266
+ JSON.stringify(
267
+ {
268
+ definitions: {},
269
+ },
270
+ null,
271
+ 2,
272
+ ) + "\n",
273
+ );
274
+ }
275
+ const whitelistData = fs.readFileSync(this.paths.prodWhitelistPath);
276
+ fs.writeFileSync(
277
+ this.paths.prodWhitelistHashPath,
278
+ JSON.stringify(
279
+ {
280
+ description,
281
+ checksum: hash(whitelistData),
282
+ "checksum-sha1": hash(whitelistData, "sha1"),
283
+ },
284
+ null,
285
+ 2,
286
+ ) + "\n",
287
+ );
288
+ if (admin && this.options.freeze) {
289
+ fs.writeFileSync(this.paths.prodFreeze, "");
290
+ } else {
291
+ if (fs.existsSync(this.paths.prodFreeze)) {
292
+ fs.rmSync(this.paths.prodFreeze);
293
+ }
294
+ }
295
+ return {
296
+ success: true,
297
+ messages: [],
298
+ };
299
+ }
300
+ }
301
+
302
+ function releasedEntityCheck(csnBuild, csnProd, whitelist, options) {
303
+ const messages = [];
304
+ visitPersistenceEntities(csnProd, (definitionProd) => {
305
+ let lookupName = definitionProd.name;
306
+ if (lookupName.startsWith("cds.xt.") && !this.options.checkMtx) {
307
+ return;
308
+ }
309
+ const definitionBuild = csnBuild.definitions[lookupName];
310
+ if (!definitionBuild) {
311
+ report(messages, MessagesCodes.ReleasedEntityCannotBeRemoved, definitionProd.name);
312
+ return;
313
+ }
314
+ if (definitionProd["@odata.draft.enabled"] !== definitionBuild["@odata.draft.enabled"]) {
315
+ report(messages, MessagesCodes.ReleasedEntityDraftEnablementCannotBeChanged, definitionProd.name);
316
+ }
317
+ const definitionWhitelist = whitelist.definitions && whitelist.definitions[definitionProd.name];
318
+ Object.keys(definitionProd.elements || {}).forEach((elementProdName) => {
319
+ const elementProd = definitionProd.elements[elementProdName];
320
+ const elementBuild = definitionBuild.elements[elementProdName];
321
+ const elementWhitelist =
322
+ definitionWhitelist && definitionWhitelist.elements && definitionWhitelist.elements[elementProdName];
323
+ if (elementBuild) {
324
+ if (["cds.Association", "cds.Composition"].includes(elementProd.type)) {
325
+ if (!((elementProd.on && elementBuild.on) || (elementProd.keys && elementBuild.keys))) {
326
+ report(
327
+ messages,
328
+ MessagesCodes.ReleasedElementManagedUnmanagedCannotBeChanged,
329
+ definitionProd.name,
330
+ elementProdName,
331
+ );
332
+ return;
333
+ }
334
+ }
335
+ }
336
+ if (elementProd.on) {
337
+ return; // Skip unmanaged association / composition
338
+ }
339
+ if (!elementBuild) {
340
+ if (!elementProd.virtual) {
341
+ report(messages, MessagesCodes.ReleasedElementCannotBeRemoved, definitionProd.name, elementProdName);
342
+ }
343
+ } else if (elementProd.key !== elementBuild.key) {
344
+ report(messages, MessagesCodes.ReleasedElementKeyCannotBeChanged, definitionProd.name, elementProdName);
345
+ } else if (elementProd.virtual !== elementBuild.virtual) {
346
+ report(messages, MessagesCodes.ReleasedElementVirtualCannotBeChanged, definitionProd.name, elementProdName);
347
+ } else if (elementProd.localized && !elementBuild.localized) {
348
+ report(
349
+ messages,
350
+ MessagesCodes.ReleasedElementLocalizationCannotBeChanged,
351
+ definitionProd.name,
352
+ elementProdName,
353
+ );
354
+ } else if (!elementProd.notNull && elementBuild.notNull) {
355
+ report(messages, MessagesCodes.ReleasedElementNullableCannotBeChanged, definitionProd.name, elementProdName);
356
+ } else if (normalizeType(csnProd, elementProd.type) !== normalizeType(csnBuild, elementBuild.type)) {
357
+ report(messages, MessagesCodes.ReleasedElementTypeCannotBeChanged, definitionProd.name, elementProdName);
358
+ } else if ((elementProd.length || STRING_DEFAULT_LENGTH) > (elementBuild.length || STRING_DEFAULT_LENGTH)) {
359
+ report(messages, MessagesCodes.ReleasedElementTypeCannotBeShortened, definitionProd.name, elementProdName);
360
+ } else if ((elementProd.length || STRING_DEFAULT_LENGTH) < (elementBuild.length || STRING_DEFAULT_LENGTH)) {
361
+ if (!elementWhitelist && options.whitelist) {
362
+ report(
363
+ messages,
364
+ MessagesCodes.ReleasedElementTypeExtensionIsNotWhitelisted,
365
+ definitionProd.name,
366
+ elementProdName,
367
+ );
368
+ }
369
+ } else if (elementProd.scale > elementBuild.scale || elementProd.precision > elementBuild.precision) {
370
+ report(
371
+ messages,
372
+ MessagesCodes.ReleasedElementScalePrecisionCannotBeLower,
373
+ definitionProd.name,
374
+ elementProdName,
375
+ );
376
+ } else if (elementProd.scale < elementBuild.scale || elementProd.precision < elementBuild.precision) {
377
+ if (!elementWhitelist && options.whitelist) {
378
+ report(
379
+ messages,
380
+ MessagesCodes.ReleasedElementScalePrecisionExtensionIsNotWhitelisted,
381
+ definitionProd.name,
382
+ elementProdName,
383
+ );
384
+ }
385
+ } else if (elementProd.target !== elementBuild.target) {
386
+ if (
387
+ isPersistenceEntity(csnProd, elementProd.target) ||
388
+ isPersistenceEntity(csnBuild, elementBuild.target) ||
389
+ JSON.stringify(entityKeyInfo(csnProd, elementProd.target)) !==
390
+ JSON.stringify(entityKeyInfo(csnBuild, elementBuild.target))
391
+ ) {
392
+ report(messages, MessagesCodes.ReleasedElementTargetCannotBeChanged, definitionProd.name, elementProdName);
393
+ }
394
+ } else if (
395
+ (elementProd.cardinality && elementProd.cardinality.max) !==
396
+ (elementBuild.cardinality && elementBuild.cardinality.max)
397
+ ) {
398
+ report(messages, MessagesCodes.ReleasedElementCardinalityCannotBeChanged, definitionProd.name, elementProdName);
399
+ } else if (JSON.stringify(elementProd.on) !== JSON.stringify(elementBuild.on)) {
400
+ if (isPersistenceEntity(csnProd, elementProd.target) || isPersistenceEntity(csnBuild, elementBuild.target)) {
401
+ report(
402
+ messages,
403
+ MessagesCodes.ReleasedElementOnConditionCannotBeChanged,
404
+ definitionProd.name,
405
+ elementProdName,
406
+ );
407
+ }
408
+ } else if (JSON.stringify(elementProd.keys) !== JSON.stringify(elementBuild.keys)) {
409
+ if (isPersistenceEntity(csnProd, elementProd.target) || isPersistenceEntity(csnBuild, elementBuild.target)) {
410
+ report(
411
+ messages,
412
+ MessagesCodes.ReleasedElementKeysConditionCannotBeChanged,
413
+ definitionProd.name,
414
+ elementProdName,
415
+ );
416
+ }
417
+ }
418
+ });
419
+ });
420
+ return messages;
421
+ }
422
+
423
+ function newEntityCheck(csnBuild, csnProd, whitelist, options) {
424
+ const messages = [];
425
+ visitPersistenceEntities(csnBuild, (definitionBuild, { draft } = {}) => {
426
+ let lookupName = definitionBuild.name;
427
+ const definitionProd = csnProd.definitions[lookupName];
428
+ const definitionWhitelist = whitelist.definitions && whitelist.definitions[definitionBuild.name];
429
+ if (!definitionProd && !definitionWhitelist && options.whitelist) {
430
+ report(messages, MessagesCodes.NewEntityIsNotWhitelisted, definitionBuild.name);
431
+ return;
432
+ }
433
+ if (definitionProd) {
434
+ Object.keys(definitionBuild.elements || {}).forEach((elementBuildName) => {
435
+ const elementBuild = definitionBuild.elements[elementBuildName];
436
+ if (elementBuild.virtual) {
437
+ return;
438
+ }
439
+ const elementProd = definitionProd.elements[elementBuildName];
440
+ const elementWhitelist =
441
+ definitionWhitelist && definitionWhitelist.elements && definitionWhitelist.elements[elementBuildName];
442
+ if (!elementProd) {
443
+ if (!elementWhitelist && options.whitelist) {
444
+ report(messages, MessagesCodes.NewEntityElementIsNotWhitelisted, definitionBuild.name, elementBuildName);
445
+ }
446
+ if (
447
+ !draft &&
448
+ elementBuild.notNull &&
449
+ (elementBuild.default === undefined || elementBuild.default?.val === null)
450
+ ) {
451
+ report(messages, MessagesCodes.NewEntityElementNotNullableDefault, definitionBuild.name, elementBuildName);
452
+ }
453
+ }
454
+ });
455
+ }
456
+ });
457
+ return messages;
458
+ }
459
+
460
+ function uniqueIndexCheck(csnBuild, csnProd, whitelist, options) {
461
+ const messages = [];
462
+ visitPersistenceEntities(csnBuild, (definitionBuild) => {
463
+ const definitionWhitelist = whitelist.definitions && whitelist.definitions[definitionBuild.name];
464
+ const definitionProd = csnProd.definitions[definitionBuild.name];
465
+ if (definitionProd) {
466
+ Object.keys(definitionBuild).forEach((key) => {
467
+ if (key.startsWith("@assert.unique.")) {
468
+ const uniqueIndexAnnotationBuild = definitionBuild[key];
469
+ const uniqueIndexAnnotationProd = definitionProd[key];
470
+ if (uniqueIndexAnnotationBuild && !uniqueIndexAnnotationProd && !definitionWhitelist && options.whitelist) {
471
+ report(messages, MessagesCodes.NewEntityIndexIsNotWhitelisted, definitionBuild.name);
472
+ } else if (uniqueIndexAnnotationBuild && uniqueIndexAnnotationProd) {
473
+ const checkProd = uniqueIndexAnnotationProd.every((indexPartProd) => {
474
+ return uniqueIndexAnnotationBuild.find((indexPartBuild) => {
475
+ return (indexPartProd["="] || indexPartProd) === (indexPartBuild["="] || indexPartBuild);
476
+ });
477
+ });
478
+ if (!checkProd) {
479
+ report(messages, MessagesCodes.ReleasedEntityIndexChangeIsNotAllowed, definitionBuild.name);
480
+ }
481
+ const checkBuild = uniqueIndexAnnotationBuild.every((indexPartBuild) => {
482
+ return uniqueIndexAnnotationProd.find((indexPartProd) => {
483
+ return (indexPartBuild["="] || indexPartBuild) === (indexPartProd["="] || indexPartProd);
484
+ });
485
+ });
486
+ if (!checkBuild && !definitionWhitelist && options.whitelist) {
487
+ report(messages, MessagesCodes.ReleasedEntityIndexChangeIsNotWhitelisted, definitionBuild.name);
488
+ }
489
+ }
490
+ }
491
+ });
492
+ }
493
+ });
494
+ return messages;
495
+ }
496
+
497
+ function visitPersistenceEntities(csn, onEntity) {
498
+ if (!onEntity) {
499
+ return;
500
+ }
501
+ const services = Object.keys(csn.definitions).filter((name) => {
502
+ return csn.definitions[name].kind === "service";
503
+ });
504
+ return Object.keys(csn.definitions).forEach((name) => {
505
+ // Normal persistence entity
506
+ const definition = csn.definitions[name];
507
+ if (
508
+ definition.kind === "entity" &&
509
+ !definition.query &&
510
+ !definition.projection &&
511
+ !definition["@cds.persistence.skip"]
512
+ ) {
513
+ definition.name = name;
514
+ onEntity(definition);
515
+ }
516
+
517
+ // Draft persistence entity
518
+ if (definition.kind === "entity" && definition["@odata.draft.enabled"]) {
519
+ const partOfService = services.find((service) => {
520
+ return name.startsWith(`${service}.`);
521
+ });
522
+ if (partOfService) {
523
+ const _compositeEntities = compositeEntities(csn.definitions, name);
524
+ _compositeEntities.forEach((name) => {
525
+ const definition = csn.definitions[name];
526
+ definition.name = name;
527
+ onEntity(definition, { draft: true });
528
+ });
529
+ }
530
+ }
531
+ });
532
+ }
533
+
534
+ function compositeEntities(csn, name, result = []) {
535
+ result.push(name);
536
+ const entity = csn[name];
537
+ if (entity && entity.elements) {
538
+ Object.keys(entity.elements).forEach((elementName) => {
539
+ const element = entity.elements[elementName];
540
+ if (element.type === "cds.Composition") {
541
+ compositeEntities(csn, element.target, result);
542
+ }
543
+ });
544
+ }
545
+ return result;
546
+ }
547
+
548
+ function isPersistenceEntity(csn, entity) {
549
+ return !(csn.definitions[entity].query || csn.definitions[entity].projection);
550
+ }
551
+
552
+ function entityKeyInfo(csn, entity) {
553
+ return Object.keys(csn.definitions[entity].elements)
554
+ .filter((name) => {
555
+ return !!csn.definitions[entity].elements[name].key;
556
+ })
557
+ .map((name) => {
558
+ return {
559
+ name,
560
+ type: csn.definitions[entity].elements[name].type,
561
+ };
562
+ }, [])
563
+ .sort((a, b) => a.name.localeCompare(b.name));
564
+ }
565
+
566
+ function report(messages, code, entity, element, severity = "error") {
567
+ const text = Messages[code];
568
+ const message = {
569
+ code,
570
+ text: `${text}: ${entity}${element ? "." + element : ""}`,
571
+ entity,
572
+ element,
573
+ severity,
574
+ };
575
+ messages.push(message);
576
+ return message;
577
+ }
578
+
579
+ function normalizeType(csn, type) {
580
+ while (csn.definitions[type]) {
581
+ type = csn.definitions[type].type;
582
+ }
583
+ return typeof type === "object" ? JSON.stringify(type) : type;
584
+ }
585
+
586
+ const hash = (buffer, algorithm = "sha256") => crypto.createHash(algorithm).update(buffer).digest("hex");
587
+
588
+ module.exports = MigrationCheck;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ module.exports = {
4
+ MigrationCheck: require("./MigrationCheck"),
5
+ };