@cratis/scene.engine 4.25.0 → 4.27.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.
Files changed (223) hide show
  1. package/dist/cjs/editing/resolveEffectiveConfiguration.js +8 -0
  2. package/dist/cjs/editing/resolveEffectiveConfiguration.js.map +1 -1
  3. package/dist/cjs/index.js +64 -0
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/navigation/NavigationDiagnosticCode.js +5 -0
  6. package/dist/cjs/navigation/NavigationDiagnosticCode.js.map +1 -1
  7. package/dist/cjs/navigation/NavigationEditKind.js +16 -0
  8. package/dist/cjs/navigation/NavigationEditKind.js.map +1 -0
  9. package/dist/cjs/navigation/NavigationItemOrigin.js +14 -0
  10. package/dist/cjs/navigation/NavigationItemOrigin.js.map +1 -0
  11. package/dist/cjs/navigation/NavigationSourcesKind.js +16 -0
  12. package/dist/cjs/navigation/NavigationSourcesKind.js.map +1 -0
  13. package/dist/cjs/navigation/diagnoseNavigation.js +8 -1
  14. package/dist/cjs/navigation/diagnoseNavigation.js.map +1 -1
  15. package/dist/cjs/navigation/diagnoseNavigationRegions.js +99 -0
  16. package/dist/cjs/navigation/diagnoseNavigationRegions.js.map +1 -0
  17. package/dist/cjs/navigation/effectiveNavigation.js +133 -0
  18. package/dist/cjs/navigation/effectiveNavigation.js.map +1 -0
  19. package/dist/cjs/navigation/navigationEdits.js +230 -0
  20. package/dist/cjs/navigation/navigationEdits.js.map +1 -0
  21. package/dist/cjs/navigation/navigationRegions.js +42 -0
  22. package/dist/cjs/navigation/navigationRegions.js.map +1 -0
  23. package/dist/cjs/sampleData/SampleRandom.js +77 -0
  24. package/dist/cjs/sampleData/SampleRandom.js.map +1 -0
  25. package/dist/cjs/sampleData/describeSampleDataCatalog.js +61 -0
  26. package/dist/cjs/sampleData/describeSampleDataCatalog.js.map +1 -0
  27. package/dist/cjs/sampleData/findSampleDataSet.js +18 -0
  28. package/dist/cjs/sampleData/findSampleDataSet.js.map +1 -0
  29. package/dist/cjs/sampleData/findSampleRowLiterals.js +73 -0
  30. package/dist/cjs/sampleData/findSampleRowLiterals.js.map +1 -0
  31. package/dist/cjs/sampleData/generateSample.js +188 -0
  32. package/dist/cjs/sampleData/generateSample.js.map +1 -0
  33. package/dist/cjs/sampleData/inferSampleFieldKind.js +176 -0
  34. package/dist/cjs/sampleData/inferSampleFieldKind.js.map +1 -0
  35. package/dist/cjs/sampleData/resolveSampleData.js +107 -0
  36. package/dist/cjs/sampleData/resolveSampleData.js.map +1 -0
  37. package/dist/cjs/sampleData/sampleDataRules.js +26 -0
  38. package/dist/cjs/sampleData/sampleDataRules.js.map +1 -0
  39. package/dist/cjs/sampleData/sampleLocales.js +359 -0
  40. package/dist/cjs/sampleData/sampleLocales.js.map +1 -0
  41. package/dist/cjs/sampleData/validateSampleDataPack.js +44 -0
  42. package/dist/cjs/sampleData/validateSampleDataPack.js.map +1 -0
  43. package/dist/cjs/sampleData/validateSampleDataPackages.js +71 -0
  44. package/dist/cjs/sampleData/validateSampleDataPackages.js.map +1 -0
  45. package/dist/cjs/sampleData/validateSampleDataSelection.js +52 -0
  46. package/dist/cjs/sampleData/validateSampleDataSelection.js.map +1 -0
  47. package/dist/cjs/validateNavigationDestinations.js +2 -0
  48. package/dist/cjs/validateNavigationDestinations.js.map +1 -1
  49. package/dist/esm/editing/resolveEffectiveConfiguration.d.ts.map +1 -1
  50. package/dist/esm/editing/resolveEffectiveConfiguration.js +8 -0
  51. package/dist/esm/editing/resolveEffectiveConfiguration.js.map +1 -1
  52. package/dist/esm/index.d.ts +1 -0
  53. package/dist/esm/index.d.ts.map +1 -1
  54. package/dist/esm/index.js +19 -0
  55. package/dist/esm/index.js.map +1 -1
  56. package/dist/esm/navigation/EffectiveNavItem.d.ts +14 -0
  57. package/dist/esm/navigation/EffectiveNavItem.d.ts.map +1 -0
  58. package/dist/esm/navigation/EffectiveNavItem.js +2 -0
  59. package/dist/esm/navigation/EffectiveNavItem.js.map +1 -0
  60. package/dist/esm/navigation/EffectiveNavigationRegion.d.ts +11 -0
  61. package/dist/esm/navigation/EffectiveNavigationRegion.d.ts.map +1 -0
  62. package/dist/esm/navigation/EffectiveNavigationRegion.js +2 -0
  63. package/dist/esm/navigation/EffectiveNavigationRegion.js.map +1 -0
  64. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts +6 -1
  65. package/dist/esm/navigation/NavigationDiagnosticCode.d.ts.map +1 -1
  66. package/dist/esm/navigation/NavigationDiagnosticCode.js +5 -0
  67. package/dist/esm/navigation/NavigationDiagnosticCode.js.map +1 -1
  68. package/dist/esm/navigation/NavigationEdit.d.ts +13 -0
  69. package/dist/esm/navigation/NavigationEdit.d.ts.map +1 -0
  70. package/dist/esm/navigation/NavigationEdit.js +2 -0
  71. package/dist/esm/navigation/NavigationEdit.js.map +1 -0
  72. package/dist/esm/navigation/NavigationEditKind.d.ts +7 -0
  73. package/dist/esm/navigation/NavigationEditKind.d.ts.map +1 -0
  74. package/dist/esm/navigation/NavigationEditKind.js +14 -0
  75. package/dist/esm/navigation/NavigationEditKind.js.map +1 -0
  76. package/dist/esm/navigation/NavigationEditOutcome.d.ts +7 -0
  77. package/dist/esm/navigation/NavigationEditOutcome.d.ts.map +1 -0
  78. package/dist/esm/navigation/NavigationEditOutcome.js +2 -0
  79. package/dist/esm/navigation/NavigationEditOutcome.js.map +1 -0
  80. package/dist/esm/navigation/NavigationEditPlan.d.ts +6 -0
  81. package/dist/esm/navigation/NavigationEditPlan.d.ts.map +1 -0
  82. package/dist/esm/navigation/NavigationEditPlan.js +2 -0
  83. package/dist/esm/navigation/NavigationEditPlan.js.map +1 -0
  84. package/dist/esm/navigation/NavigationGraph.d.ts +4 -1
  85. package/dist/esm/navigation/NavigationGraph.d.ts.map +1 -1
  86. package/dist/esm/navigation/NavigationItemOrigin.d.ts +5 -0
  87. package/dist/esm/navigation/NavigationItemOrigin.d.ts.map +1 -0
  88. package/dist/esm/navigation/NavigationItemOrigin.js +12 -0
  89. package/dist/esm/navigation/NavigationItemOrigin.js.map +1 -0
  90. package/dist/esm/navigation/NavigationSources.d.ts +6 -0
  91. package/dist/esm/navigation/NavigationSources.d.ts.map +1 -0
  92. package/dist/esm/navigation/NavigationSources.js +2 -0
  93. package/dist/esm/navigation/NavigationSources.js.map +1 -0
  94. package/dist/esm/navigation/NavigationSourcesKind.d.ts +7 -0
  95. package/dist/esm/navigation/NavigationSourcesKind.d.ts.map +1 -0
  96. package/dist/esm/navigation/NavigationSourcesKind.js +14 -0
  97. package/dist/esm/navigation/NavigationSourcesKind.js.map +1 -0
  98. package/dist/esm/navigation/ResolvedNavigationTarget.d.ts +6 -0
  99. package/dist/esm/navigation/ResolvedNavigationTarget.d.ts.map +1 -0
  100. package/dist/esm/navigation/ResolvedNavigationTarget.js +2 -0
  101. package/dist/esm/navigation/ResolvedNavigationTarget.js.map +1 -0
  102. package/dist/esm/navigation/diagnoseNavigation.d.ts.map +1 -1
  103. package/dist/esm/navigation/diagnoseNavigation.js +8 -1
  104. package/dist/esm/navigation/diagnoseNavigation.js.map +1 -1
  105. package/dist/esm/navigation/diagnoseNavigationRegions.d.ts +8 -0
  106. package/dist/esm/navigation/diagnoseNavigationRegions.d.ts.map +1 -0
  107. package/dist/esm/navigation/diagnoseNavigationRegions.js +97 -0
  108. package/dist/esm/navigation/diagnoseNavigationRegions.js.map +1 -0
  109. package/dist/esm/navigation/effectiveNavigation.d.ts +5 -0
  110. package/dist/esm/navigation/effectiveNavigation.d.ts.map +1 -0
  111. package/dist/esm/navigation/effectiveNavigation.js +131 -0
  112. package/dist/esm/navigation/effectiveNavigation.js.map +1 -0
  113. package/dist/esm/navigation/index.d.ts +14 -0
  114. package/dist/esm/navigation/index.d.ts.map +1 -1
  115. package/dist/esm/navigation/index.js +14 -0
  116. package/dist/esm/navigation/index.js.map +1 -1
  117. package/dist/esm/navigation/navigationEdits.d.ts +11 -0
  118. package/dist/esm/navigation/navigationEdits.d.ts.map +1 -0
  119. package/dist/esm/navigation/navigationEdits.js +225 -0
  120. package/dist/esm/navigation/navigationEdits.js.map +1 -0
  121. package/dist/esm/navigation/navigationRegions.d.ts +9 -0
  122. package/dist/esm/navigation/navigationRegions.d.ts.map +1 -0
  123. package/dist/esm/navigation/navigationRegions.js +34 -0
  124. package/dist/esm/navigation/navigationRegions.js.map +1 -0
  125. package/dist/esm/sampleData/SampleDataPackSource.d.ts +6 -0
  126. package/dist/esm/sampleData/SampleDataPackSource.d.ts.map +1 -0
  127. package/dist/esm/sampleData/SampleDataPackSource.js +2 -0
  128. package/dist/esm/sampleData/SampleDataPackSource.js.map +1 -0
  129. package/dist/esm/sampleData/SampleDataResolution.d.ts +29 -0
  130. package/dist/esm/sampleData/SampleDataResolution.d.ts.map +1 -0
  131. package/dist/esm/sampleData/SampleDataResolution.js +2 -0
  132. package/dist/esm/sampleData/SampleDataResolution.js.map +1 -0
  133. package/dist/esm/sampleData/SampleRandom.d.ts +14 -0
  134. package/dist/esm/sampleData/SampleRandom.d.ts.map +1 -0
  135. package/dist/esm/sampleData/SampleRandom.js +72 -0
  136. package/dist/esm/sampleData/SampleRandom.js.map +1 -0
  137. package/dist/esm/sampleData/describeSampleDataCatalog.d.ts +24 -0
  138. package/dist/esm/sampleData/describeSampleDataCatalog.d.ts.map +1 -0
  139. package/dist/esm/sampleData/describeSampleDataCatalog.js +57 -0
  140. package/dist/esm/sampleData/describeSampleDataCatalog.js.map +1 -0
  141. package/dist/esm/sampleData/findSampleDataSet.d.ts +3 -0
  142. package/dist/esm/sampleData/findSampleDataSet.d.ts.map +1 -0
  143. package/dist/esm/sampleData/findSampleDataSet.js +16 -0
  144. package/dist/esm/sampleData/findSampleDataSet.js.map +1 -0
  145. package/dist/esm/sampleData/findSampleRowLiterals.d.ts +11 -0
  146. package/dist/esm/sampleData/findSampleRowLiterals.d.ts.map +1 -0
  147. package/dist/esm/sampleData/findSampleRowLiterals.js +69 -0
  148. package/dist/esm/sampleData/findSampleRowLiterals.js.map +1 -0
  149. package/dist/esm/sampleData/generateSample.d.ts +6 -0
  150. package/dist/esm/sampleData/generateSample.d.ts.map +1 -0
  151. package/dist/esm/sampleData/generateSample.js +183 -0
  152. package/dist/esm/sampleData/generateSample.js.map +1 -0
  153. package/dist/esm/sampleData/index.d.ts +15 -0
  154. package/dist/esm/sampleData/index.d.ts.map +1 -0
  155. package/dist/esm/sampleData/index.js +15 -0
  156. package/dist/esm/sampleData/index.js.map +1 -0
  157. package/dist/esm/sampleData/inferSampleFieldKind.d.ts +3 -0
  158. package/dist/esm/sampleData/inferSampleFieldKind.d.ts.map +1 -0
  159. package/dist/esm/sampleData/inferSampleFieldKind.js +174 -0
  160. package/dist/esm/sampleData/inferSampleFieldKind.js.map +1 -0
  161. package/dist/esm/sampleData/resolveSampleData.d.ts +3 -0
  162. package/dist/esm/sampleData/resolveSampleData.d.ts.map +1 -0
  163. package/dist/esm/sampleData/resolveSampleData.js +105 -0
  164. package/dist/esm/sampleData/resolveSampleData.js.map +1 -0
  165. package/dist/esm/sampleData/sampleDataRules.d.ts +7 -0
  166. package/dist/esm/sampleData/sampleDataRules.d.ts.map +1 -0
  167. package/dist/esm/sampleData/sampleDataRules.js +19 -0
  168. package/dist/esm/sampleData/sampleDataRules.js.map +1 -0
  169. package/dist/esm/sampleData/sampleLocales.d.ts +13 -0
  170. package/dist/esm/sampleData/sampleLocales.d.ts.map +1 -0
  171. package/dist/esm/sampleData/sampleLocales.js +355 -0
  172. package/dist/esm/sampleData/sampleLocales.js.map +1 -0
  173. package/dist/esm/sampleData/validateSampleDataPack.d.ts +3 -0
  174. package/dist/esm/sampleData/validateSampleDataPack.d.ts.map +1 -0
  175. package/dist/esm/sampleData/validateSampleDataPack.js +42 -0
  176. package/dist/esm/sampleData/validateSampleDataPack.js.map +1 -0
  177. package/dist/esm/sampleData/validateSampleDataPackages.d.ts +3 -0
  178. package/dist/esm/sampleData/validateSampleDataPackages.d.ts.map +1 -0
  179. package/dist/esm/sampleData/validateSampleDataPackages.js +69 -0
  180. package/dist/esm/sampleData/validateSampleDataPackages.js.map +1 -0
  181. package/dist/esm/sampleData/validateSampleDataSelection.d.ts +3 -0
  182. package/dist/esm/sampleData/validateSampleDataSelection.d.ts.map +1 -0
  183. package/dist/esm/sampleData/validateSampleDataSelection.js +50 -0
  184. package/dist/esm/sampleData/validateSampleDataSelection.js.map +1 -0
  185. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  186. package/dist/esm/validateNavigationDestinations.js +2 -0
  187. package/dist/esm/validateNavigationDestinations.js.map +1 -1
  188. package/editing/resolveEffectiveConfiguration.ts +13 -0
  189. package/index.ts +1 -0
  190. package/navigation/EffectiveNavItem.ts +37 -0
  191. package/navigation/EffectiveNavigationRegion.ts +26 -0
  192. package/navigation/NavigationDiagnosticCode.ts +15 -0
  193. package/navigation/NavigationEdit.ts +36 -0
  194. package/navigation/NavigationEditKind.ts +19 -0
  195. package/navigation/NavigationEditOutcome.ts +18 -0
  196. package/navigation/NavigationEditPlan.ts +15 -0
  197. package/navigation/NavigationGraph.ts +10 -1
  198. package/navigation/NavigationItemOrigin.ts +13 -0
  199. package/navigation/NavigationSources.ts +15 -0
  200. package/navigation/NavigationSourcesKind.ts +19 -0
  201. package/navigation/ResolvedNavigationTarget.ts +19 -0
  202. package/navigation/diagnoseNavigation.ts +7 -1
  203. package/navigation/diagnoseNavigationRegions.ts +122 -0
  204. package/navigation/effectiveNavigation.ts +105 -0
  205. package/navigation/index.ts +14 -0
  206. package/navigation/navigationEdits.ts +176 -0
  207. package/navigation/navigationRegions.ts +39 -0
  208. package/package.json +3 -3
  209. package/sampleData/SampleDataPackSource.ts +13 -0
  210. package/sampleData/SampleDataResolution.ts +93 -0
  211. package/sampleData/SampleRandom.ts +85 -0
  212. package/sampleData/describeSampleDataCatalog.ts +108 -0
  213. package/sampleData/findSampleDataSet.ts +18 -0
  214. package/sampleData/findSampleRowLiterals.ts +86 -0
  215. package/sampleData/generateSample.ts +156 -0
  216. package/sampleData/index.ts +17 -0
  217. package/sampleData/inferSampleFieldKind.ts +64 -0
  218. package/sampleData/resolveSampleData.ts +96 -0
  219. package/sampleData/sampleDataRules.ts +30 -0
  220. package/sampleData/sampleLocales.ts +91 -0
  221. package/sampleData/validateSampleDataPack.ts +48 -0
  222. package/sampleData/validateSampleDataPackages.ts +79 -0
  223. package/sampleData/validateSampleDataSelection.ts +55 -0
@@ -0,0 +1,156 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { SampleFieldKind, SampleShape, SampleShapeField } from '@cratis/scene.model';
5
+ import { inferSampleFieldKind } from './inferSampleFieldKind';
6
+ import { SampleLocale, sampleLocaleFor } from './sampleLocales';
7
+ import { SampleRandom, rowSeed } from './SampleRandom';
8
+
9
+ /** The seed used when none is given, so previews are stable without any configuration. */
10
+ export const defaultSampleSeed = 1;
11
+
12
+ /** How many rows are generated when no count is given. */
13
+ export const defaultSampleRowCount = 5;
14
+
15
+ /** The most rows one call generates. */
16
+ export const maximumSampleRowCount = 1000;
17
+
18
+ /** Days since 1970-01-01 of 2024-01-01, the anchor generated dates count forward from. */
19
+ const anchorDay = 19723;
20
+
21
+ /** Generated dates fall within this many days after the anchor. */
22
+ const dateSpanDays = 731;
23
+
24
+ /**
25
+ * Generates `rowCount` rows of a shape, deterministically for a seed: the same shape, seed, row count and
26
+ * locale yield the same rows (byte-identical once serialized) in the C# and TypeScript engines.
27
+ *
28
+ * Each row has its own generator (see {@link rowSeed}), so row `i` is the same however many rows are asked
29
+ * for. Within a row the fields are generated in declaration order. A nullable field first draws whether it is
30
+ * null (one in five is) and then always draws its value, so the sequence never depends on nullability
31
+ * outcomes. Values follow the field's kind and locale; enum fields only ever produce one of their values,
32
+ * dates stay within 2024-2025 and amounts have two decimals.
33
+ *
34
+ * @param shape The shape to generate rows for.
35
+ * @param seed The seed; defaults to {@link defaultSampleSeed}.
36
+ * @param rowCount The number of rows; defaults to {@link defaultSampleRowCount}, clamped to `[0, 1000]`.
37
+ * @param locale The BCP 47 locale tag selecting names, cities and formats; unknown tags use the default pool.
38
+ */
39
+ export function generateSample(shape: SampleShape, seed?: number, rowCount?: number, locale?: string): Record<string, unknown>[] {
40
+ const count = Math.min(Math.max(Math.trunc(rowCount ?? defaultSampleRowCount), 0), maximumSampleRowCount);
41
+ const effectiveSeed = (seed ?? defaultSampleSeed) >>> 0;
42
+ const pool = sampleLocaleFor(locale);
43
+ const rows: Record<string, unknown>[] = [];
44
+
45
+ for (let index = 0; index < count; index++) {
46
+ const random = new SampleRandom(rowSeed(effectiveSeed, shape.name, index));
47
+ const row: Record<string, unknown> = {};
48
+ for (const field of shape.fields) {
49
+ const isNull = field.nullable === true && random.nextInt(5) === 0;
50
+ const value = generateValue(field, inferSampleFieldKind(field), random, pool);
51
+ row[field.name] = isNull ? null : value;
52
+ }
53
+ rows.push(row);
54
+ }
55
+
56
+ return rows;
57
+ }
58
+
59
+ function generateValue(field: SampleShapeField, kind: SampleFieldKind, random: SampleRandom, pool: SampleLocale): unknown {
60
+ switch (kind) {
61
+ case SampleFieldKind.Id:
62
+ return generateId(random);
63
+ case SampleFieldKind.Name:
64
+ return `${random.pick(pool.firstNames)} ${random.pick(pool.lastNames)}`;
65
+ case SampleFieldKind.Company:
66
+ return random.pick(pool.companies);
67
+ case SampleFieldKind.Email: {
68
+ const first = random.pick(pool.firstNames);
69
+ const last = random.pick(pool.lastNames);
70
+ return `${slug(first)}.${slug(last)}@${random.pick(pool.domains)}`;
71
+ }
72
+ case SampleFieldKind.Phone:
73
+ return [...pool.phoneTemplate].map(character => (character === '#' ? String(random.nextInt(10)) : character)).join('');
74
+ case SampleFieldKind.City:
75
+ return random.pick(pool.cities);
76
+ case SampleFieldKind.Text: {
77
+ const words = Array.from({ length: 3 + random.nextInt(4) }, () => random.pick(pool.words));
78
+ return capitalize(words.join(' '));
79
+ }
80
+ case SampleFieldKind.WholeNumber: {
81
+ const [low, high] = range(field.min, field.max, 0, 1000, Math.trunc);
82
+ return low + random.nextInt(high - low + 1);
83
+ }
84
+ case SampleFieldKind.Amount: {
85
+ const [low, high] = range(field.min, field.max, 10, 5000, value => Math.floor(value * 100 + 0.5));
86
+ return (low + random.nextInt(high - low + 1)) / 100;
87
+ }
88
+ case SampleFieldKind.Boolean:
89
+ return random.nextInt(2) === 1;
90
+ case SampleFieldKind.Date:
91
+ return formatDay(anchorDay + random.nextInt(dateSpanDays));
92
+ case SampleFieldKind.DateTime: {
93
+ const day = anchorDay + random.nextInt(dateSpanDays);
94
+ const seconds = random.nextInt(86400);
95
+ return `${formatDay(day)}T${pad(Math.floor(seconds / 3600))}:${pad(Math.floor(seconds / 60) % 60)}:${pad(seconds % 60)}Z`;
96
+ }
97
+ case SampleFieldKind.Enum:
98
+ return field.enumValues !== undefined && field.enumValues !== null && field.enumValues.length > 0 ? random.pick(field.enumValues) : null;
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function range(min: number | undefined, max: number | undefined, defaultMin: number, defaultMax: number, convert: (value: number) => number): [number, number] {
105
+ const low = convert(min ?? defaultMin);
106
+ const high = convert(max ?? defaultMax);
107
+ return high < low ? [high, low] : [low, high];
108
+ }
109
+
110
+ function generateId(random: SampleRandom): string {
111
+ const a = random.next();
112
+ const b = ((random.next() & 0xFFFF0FFF) | 0x00004000) >>> 0;
113
+ const c = ((random.next() & 0x3FFFFFFF) | 0x80000000) >>> 0;
114
+ const d = random.next();
115
+ return `${hex(a, 8)}-${hex(b >>> 16, 4)}-${hex(b & 0xFFFF, 4)}-${hex(c >>> 16, 4)}-${hex(c & 0xFFFF, 4)}${hex(d, 8)}`;
116
+ }
117
+
118
+ const transliterations = new Map<string, string>([['æ', 'ae'], ['ø', 'o'], ['å', 'a'], ['ä', 'a'], ['ö', 'o'], ['ü', 'u'], ['ß', 'ss'], ['é', 'e']]);
119
+
120
+ function slug(text: string): string {
121
+ let result = '';
122
+ for (const character of text.toLowerCase()) {
123
+ const mapped = transliterations.get(character) ?? character;
124
+ for (const part of mapped) {
125
+ if ((part >= 'a' && part <= 'z') || (part >= '0' && part <= '9')) result += part;
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+
131
+ function capitalize(text: string): string {
132
+ const first = text.charAt(0);
133
+ return first >= 'a' && first <= 'z' ? first.toUpperCase() + text.slice(1) : text;
134
+ }
135
+
136
+ function hex(value: number, digits: number): string {
137
+ return (value >>> 0).toString(16).padStart(digits, '0');
138
+ }
139
+
140
+ function pad(value: number): string {
141
+ return value < 10 ? `0${value}` : String(value);
142
+ }
143
+
144
+ /** Formats days since 1970-01-01 as `yyyy-MM-dd` (Howard Hinnant's civil-from-days). */
145
+ function formatDay(days: number): string {
146
+ const shifted = days + 719468;
147
+ const era = Math.floor(shifted / 146097);
148
+ const dayOfEra = shifted - era * 146097;
149
+ const yearOfEra = Math.floor((dayOfEra - Math.floor(dayOfEra / 1460) + Math.floor(dayOfEra / 36524) - Math.floor(dayOfEra / 146096)) / 365);
150
+ const dayOfYear = dayOfEra - (365 * yearOfEra + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100));
151
+ const monthPrime = Math.floor((5 * dayOfYear + 2) / 153);
152
+ const day = dayOfYear - Math.floor((153 * monthPrime + 2) / 5) + 1;
153
+ const month = monthPrime < 10 ? monthPrime + 3 : monthPrime - 9;
154
+ const year = yearOfEra + era * 400 + (month <= 2 ? 1 : 0);
155
+ return `${String(year).padStart(4, '0')}-${pad(month)}-${pad(day)}`;
156
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ export * from './SampleRandom';
5
+ export * from './sampleLocales';
6
+ export * from './inferSampleFieldKind';
7
+ export * from './generateSample';
8
+ export * from './SampleDataResolution';
9
+ export * from './findSampleDataSet';
10
+ export * from './resolveSampleData';
11
+ export * from './sampleDataRules';
12
+ export * from './validateSampleDataPack';
13
+ export * from './validateSampleDataSelection';
14
+ export * from './SampleDataPackSource';
15
+ export * from './validateSampleDataPackages';
16
+ export * from './describeSampleDataCatalog';
17
+ export * from './findSampleRowLiterals';
@@ -0,0 +1,64 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { SampleFieldKind, SampleShapeField } from '@cratis/scene.model';
5
+
6
+ const typeKinds = new Map<string, SampleFieldKind>([
7
+ ['guid', SampleFieldKind.Id],
8
+ ['uuid', SampleFieldKind.Id],
9
+ ['int', SampleFieldKind.WholeNumber],
10
+ ['int16', SampleFieldKind.WholeNumber],
11
+ ['int32', SampleFieldKind.WholeNumber],
12
+ ['int64', SampleFieldKind.WholeNumber],
13
+ ['short', SampleFieldKind.WholeNumber],
14
+ ['long', SampleFieldKind.WholeNumber],
15
+ ['integer', SampleFieldKind.WholeNumber],
16
+ ['number', SampleFieldKind.WholeNumber],
17
+ ['decimal', SampleFieldKind.Amount],
18
+ ['double', SampleFieldKind.Amount],
19
+ ['float', SampleFieldKind.Amount],
20
+ ['single', SampleFieldKind.Amount],
21
+ ['money', SampleFieldKind.Amount],
22
+ ['currency', SampleFieldKind.Amount],
23
+ ['bool', SampleFieldKind.Boolean],
24
+ ['boolean', SampleFieldKind.Boolean],
25
+ ['date', SampleFieldKind.Date],
26
+ ['dateonly', SampleFieldKind.Date],
27
+ ['datetime', SampleFieldKind.DateTime],
28
+ ['datetimeoffset', SampleFieldKind.DateTime],
29
+ ['timestamp', SampleFieldKind.DateTime],
30
+ ]);
31
+
32
+ const nameKinds: [string[], SampleFieldKind][] = [
33
+ [['email'], SampleFieldKind.Email],
34
+ [['phone', 'mobile'], SampleFieldKind.Phone],
35
+ [['company', 'organization', 'organisation'], SampleFieldKind.Company],
36
+ [['city', 'town'], SampleFieldKind.City],
37
+ [['product', 'item', 'title', 'subject', 'description'], SampleFieldKind.Text],
38
+ [['name', 'customer', 'assignee', 'owner', 'author', 'contact', 'manager', 'employee'], SampleFieldKind.Name],
39
+ ];
40
+
41
+ /**
42
+ * The kind of value to generate for a field. An explicit `kind` wins; otherwise a field with `enumValues` is
43
+ * an enum, a recognised `type` decides, and failing that the field's name does (an `Id` suffix, `email`,
44
+ * `phone`, `company`, `city`, person-like names), with free text as the fallback. A trailing `?` on the type
45
+ * is ignored and type names compare case-insensitively.
46
+ */
47
+ export function inferSampleFieldKind(field: SampleShapeField): SampleFieldKind {
48
+ if (field.kind !== undefined && field.kind !== null) return field.kind as SampleFieldKind;
49
+ if (field.enumValues !== undefined && field.enumValues !== null && field.enumValues.length > 0) return SampleFieldKind.Enum;
50
+
51
+ let type = (field.type ?? '').trim().toLowerCase();
52
+ if (type.endsWith('?')) type = type.slice(0, -1);
53
+ const byType = typeKinds.get(type);
54
+ if (byType !== undefined) return byType;
55
+
56
+ const name = field.name ?? '';
57
+ const lower = name.toLowerCase();
58
+ if (lower === 'id' || (name.length > 2 && name.endsWith('Id'))) return SampleFieldKind.Id;
59
+ for (const [fragments, kind] of nameKinds) {
60
+ if (fragments.some(fragment => lower.includes(fragment))) return kind;
61
+ }
62
+
63
+ return SampleFieldKind.Text;
64
+ }
@@ -0,0 +1,96 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { SampleDataPack } from '@cratis/scene.model';
5
+ import { generateSample } from './generateSample';
6
+ import { defaultSampleLocale } from './sampleLocales';
7
+ import { findSampleDataSet } from './findSampleDataSet';
8
+ import { SampleDataContext, SampleDataDiagnostic, SampleDataRequest, SampleDataResolution } from './SampleDataResolution';
9
+
10
+ /**
11
+ * Resolves the rows to show for a read model or shape at design or preview time, by the order of spec §11.2:
12
+ *
13
+ * 1. the pack bound to the target (`profile.sampleData.bindings`, a rare override);
14
+ * 2. the application's active pack: the UI profile's `sampleData.activePack`, chosen once for the whole
15
+ * application, or the session-only override (`context.sessionActivePack`) the preview strip sets;
16
+ * 3. the default pack of the blueprint the template came from (`blueprintDefaultPack`);
17
+ * 4. generated data typed by field kind ({@link generateSample}), when the request carries a shape.
18
+ *
19
+ * A pack supplies rows when it has a set for the target, by an explicit `shape → read model` mapping or else by
20
+ * name ({@link findSampleDataSet}). A rung that cannot supply rows falls through to the next; an unknown pack
21
+ * id, or a bound pack without the target's shape, is reported in `diagnostics` (an active or blueprint pack
22
+ * that merely lacks the shape is normal and not reported). The locale carried is the supplying pack's; for
23
+ * generated data it is the first existing bound, active or blueprint pack's, else `context.locale`.
24
+ *
25
+ * Outside design and preview (`context.isDesignOrPreview` not true) nothing resolves (spec SD6).
26
+ */
27
+ export function resolveSampleData(request: SampleDataRequest, context: SampleDataContext): SampleDataResolution {
28
+ const target = request.target ?? request.shape?.name ?? '';
29
+ const fallbackLocale = context.locale ?? defaultSampleLocale;
30
+ const diagnostics: SampleDataDiagnostic[] = [];
31
+
32
+ if (context.isDesignOrPreview !== true) {
33
+ return { source: 'disabled', locale: fallbackLocale, rows: [], diagnostics };
34
+ }
35
+
36
+ const packs = context.packs ?? [];
37
+ const selection = context.profile?.sampleData;
38
+ const mappings = selection?.mappings ?? [];
39
+ const find = (id: string | null | undefined): SampleDataPack | undefined => (id === undefined || id === null ? undefined : packs.find(pack => pack.id === id));
40
+ const missing = (id: string, role: string): void => {
41
+ diagnostics.push({ code: 'packNotFound', message: `The ${role} sample data pack '${id}' is not available` });
42
+ };
43
+
44
+ const bindingPackId = (selection?.bindings ?? []).find(binding => binding.target === target)?.pack;
45
+ const bindingPack = find(bindingPackId);
46
+ if (bindingPackId !== undefined && bindingPack === undefined) {
47
+ missing(bindingPackId, `bound`);
48
+ } else if (bindingPack !== undefined) {
49
+ const set = findSampleDataSet(bindingPack, target, mappings);
50
+ if (set !== undefined) return fromSet('binding', bindingPack, set.shape, set.rows, diagnostics);
51
+ diagnostics.push({ code: 'shapeNotInPack', message: `The bound sample data pack '${bindingPack.id}' has no rows for '${target}'` });
52
+ }
53
+
54
+ const sessionPackId = context.sessionActivePack;
55
+ const chosenPackId = sessionPackId !== undefined ? sessionPackId : selection?.activePack;
56
+ const activePackId = chosenPackId === '' ? undefined : chosenPackId;
57
+ const activePack = find(activePackId);
58
+ if (activePackId !== undefined && activePackId !== null && activePack === undefined) {
59
+ missing(activePackId, 'active');
60
+ } else if (activePack !== undefined) {
61
+ const set = findSampleDataSet(activePack, target, mappings);
62
+ if (set !== undefined) return fromSet('activePack', activePack, set.shape, set.rows, diagnostics);
63
+ }
64
+
65
+ const blueprintPackId = context.blueprintDefaultPack;
66
+ const blueprintPack = find(blueprintPackId);
67
+ if (blueprintPackId !== undefined && blueprintPackId !== null && blueprintPack === undefined) {
68
+ missing(blueprintPackId, 'blueprint default');
69
+ } else if (blueprintPack !== undefined) {
70
+ const set = findSampleDataSet(blueprintPack, target, mappings);
71
+ if (set !== undefined) return fromSet('blueprintDefault', blueprintPack, set.shape, set.rows, diagnostics);
72
+ }
73
+
74
+ const locale = bindingPack?.locale ?? activePack?.locale ?? blueprintPack?.locale ?? fallbackLocale;
75
+ if (request.shape === undefined) {
76
+ return { source: 'none', locale, shape: target, rows: [], diagnostics };
77
+ }
78
+
79
+ return {
80
+ source: 'generated',
81
+ locale,
82
+ shape: request.shape.name,
83
+ rows: generateSample(request.shape, context.seed, context.rowCount, locale),
84
+ diagnostics,
85
+ };
86
+ }
87
+
88
+ function fromSet(
89
+ source: 'binding' | 'activePack' | 'blueprintDefault',
90
+ pack: SampleDataPack,
91
+ shape: string,
92
+ rows: Record<string, unknown>[],
93
+ diagnostics: SampleDataDiagnostic[],
94
+ ): SampleDataResolution {
95
+ return { source, pack: pack.id, locale: pack.locale, shape, rows: structuredClone(rows), diagnostics };
96
+ }
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** A pack id: lower-case letters and digits in groups separated by `.`, `_` or `-` (`crm-nordic-smb`). */
5
+ export const sampleDataIdPattern = /^[a-z0-9]+([._-][a-z0-9]+)*$/;
6
+
7
+ /** A BCP 47 style locale tag: a 2-3 letter language followed by alphanumeric subtags (`nb-NO`, `en-GB`, `zh-Hant-TW`). */
8
+ export const sampleDataLocalePattern = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
9
+
10
+ /** The owner of a pack the author created. */
11
+ export const userSampleDataOwner = 'user';
12
+
13
+ /** The prefix of the owner of a pack a blueprint ships (`blueprint:crm`). */
14
+ export const blueprintSampleDataOwnerPrefix = 'blueprint:';
15
+
16
+ /** Whether an owner is `user` or `blueprint:<id>` with a non-empty id. */
17
+ export function isValidSampleDataOwner(owner: string | undefined): boolean {
18
+ if (owner === undefined || owner === null) return false;
19
+ return owner === userSampleDataOwner || (owner.startsWith(blueprintSampleDataOwnerPrefix) && owner.length > blueprintSampleDataOwnerPrefix.length && owner.trim() === owner);
20
+ }
21
+
22
+ /** The path separator, built from its code so the seam rule against route literals (I4) does not mistake it for a URL. */
23
+ const pathSeparator = String.fromCharCode(47);
24
+
25
+ /** Whether a package-relative pack path is `sampleData/<name>.json` with no `..`, `\\` or nested leading `/`. */
26
+ export function isValidSampleDataPath(path: string | undefined): boolean {
27
+ if (path === undefined || path === null) return false;
28
+ if (!path.startsWith('sampleData/') || !path.endsWith('.json') || path.length <= 'sampleData/.json'.length) return false;
29
+ return !path.includes('..') && !path.includes('\\') && !path.split(pathSeparator).includes('');
30
+ }
@@ -0,0 +1,91 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** The value pools and formats generated data draws from for one locale. */
5
+ export interface SampleLocale {
6
+ firstNames: string[];
7
+ lastNames: string[];
8
+ companies: string[];
9
+ cities: string[];
10
+ domains: string[];
11
+ /** `#` is replaced by a random digit. */
12
+ phoneTemplate: string;
13
+ words: string[];
14
+ }
15
+
16
+ const englishWords = ['review', 'quarterly', 'update', 'invoice', 'customer', 'delivery', 'schedule', 'renewal', 'report', 'follow', 'up', 'meeting'];
17
+
18
+ const locales = new Map<string, SampleLocale>(Object.entries({
19
+ 'en-GB': {
20
+ firstNames: ['Oliver', 'Amelia', 'George', 'Isla', 'Harry', 'Ava', 'Jack', 'Freya', 'Arthur', 'Poppy'],
21
+ lastNames: ['Smith', 'Jones', 'Taylor', 'Brown', 'Wilson', 'Evans', 'Thomas', 'Roberts', 'Walker', 'Wright'],
22
+ companies: ['Northgate Ltd', 'Albion Supplies Ltd', 'Thames Logistics Ltd', 'Pennine Foods Ltd', 'Harbour Digital Ltd', 'Cotswold Tools Ltd'],
23
+ cities: ['London', 'Manchester', 'Leeds', 'Bristol', 'Glasgow', 'Cardiff', 'Sheffield', 'Newcastle'],
24
+ domains: ['example.co.uk', 'example.org.uk', 'sample.co.uk'],
25
+ phoneTemplate: '+44 7### ######',
26
+ words: englishWords,
27
+ },
28
+ 'en-US': {
29
+ firstNames: ['James', 'Emma', 'Michael', 'Olivia', 'William', 'Sophia', 'Benjamin', 'Mia', 'Henry', 'Grace'],
30
+ lastNames: ['Johnson', 'Miller', 'Davis', 'Garcia', 'Anderson', 'Martin', 'Clark', 'Lewis', 'Young', 'Hall'],
31
+ companies: ['Summit Supply Inc', 'Bluebird Software Inc', 'Redwood Freight Inc', 'Lakeside Foods Inc', 'Cardinal Tools Inc', 'Pioneer Digital Inc'],
32
+ cities: ['New York', 'Chicago', 'Austin', 'Seattle', 'Denver', 'Boston', 'Atlanta', 'Portland'],
33
+ domains: ['example.com', 'example.net', 'sample.com'],
34
+ phoneTemplate: '+1 (555) ###-####',
35
+ words: englishWords,
36
+ },
37
+ 'nb-NO': {
38
+ firstNames: ['Lars', 'Ingrid', 'Ola', 'Kari', 'Henrik', 'Sigrid', 'Magnus', 'Åse', 'Bjørn', 'Mari'],
39
+ lastNames: ['Hansen', 'Johansen', 'Olsen', 'Larsen', 'Andersen', 'Pedersen', 'Nilsen', 'Kristiansen', 'Berg', 'Haugen'],
40
+ companies: ['Fjordline AS', 'Nordlys Handel AS', 'Fjellvik Maskin AS', 'Bergen Logistikk AS', 'Havbruk Partner AS', 'Skagerrak Digital AS'],
41
+ cities: ['Oslo', 'Bergen', 'Trondheim', 'Stavanger', 'Tromsø', 'Drammen', 'Kristiansand', 'Ålesund'],
42
+ domains: ['eksempel.no', 'prove.no', 'demo.no'],
43
+ phoneTemplate: '+47 9## ## ###',
44
+ words: ['gjennomgang', 'kvartal', 'oppdatering', 'faktura', 'kunde', 'levering', 'avtale', 'fornyelse', 'rapport', 'oppfølging', 'møte', 'bestilling'],
45
+ },
46
+ 'sv-SE': {
47
+ firstNames: ['Erik', 'Astrid', 'Lars', 'Maja', 'Anders', 'Elsa', 'Johan', 'Linnéa', 'Gustav', 'Åsa'],
48
+ lastNames: ['Andersson', 'Johansson', 'Karlsson', 'Nilsson', 'Eriksson', 'Larsson', 'Olsson', 'Persson', 'Svensson', 'Gustafsson'],
49
+ companies: ['Norrlands Handel AB', 'Östersund Teknik AB', 'Göta Logistik AB', 'Skärgården Livs AB', 'Malmö Digital AB', 'Vättern Verktyg AB'],
50
+ cities: ['Stockholm', 'Göteborg', 'Malmö', 'Uppsala', 'Västerås', 'Örebro', 'Linköping', 'Umeå'],
51
+ domains: ['exempel.se', 'prov.se', 'demo.se'],
52
+ phoneTemplate: '+46 70 ### ## ##',
53
+ words: ['granskning', 'kvartal', 'uppdatering', 'faktura', 'kund', 'leverans', 'avtal', 'förnyelse', 'rapport', 'uppföljning', 'möte', 'beställning'],
54
+ },
55
+ 'de-DE': {
56
+ firstNames: ['Lukas', 'Anna', 'Felix', 'Marie', 'Jonas', 'Sophie', 'Maximilian', 'Lena', 'Paul', 'Jördis'],
57
+ lastNames: ['Müller', 'Schmidt', 'Schneider', 'Fischer', 'Weber', 'Meyer', 'Wagner', 'Becker', 'Schulz', 'Hoffmann'],
58
+ companies: ['Rheinwerk GmbH', 'Nordsee Handel GmbH', 'Isar Logistik GmbH', 'Schwarzwald Technik GmbH', 'Elbe Digital GmbH', 'Bayern Werkzeug GmbH'],
59
+ cities: ['Berlin', 'Hamburg', 'München', 'Köln', 'Frankfurt', 'Stuttgart', 'Düsseldorf', 'Leipzig'],
60
+ domains: ['beispiel.de', 'muster.de', 'demo.de'],
61
+ phoneTemplate: '+49 151 ########',
62
+ words: ['prüfung', 'quartal', 'aktualisierung', 'rechnung', 'kunde', 'lieferung', 'vertrag', 'verlängerung', 'bericht', 'nachfassen', 'termin', 'bestellung'],
63
+ },
64
+ } as Record<string, SampleLocale>));
65
+
66
+ const languageDefaults = new Map<string, string>([
67
+ ['en', 'en-GB'],
68
+ ['nb', 'nb-NO'],
69
+ ['nn', 'nb-NO'],
70
+ ['no', 'nb-NO'],
71
+ ['sv', 'sv-SE'],
72
+ ['de', 'de-DE'],
73
+ ]);
74
+
75
+ /** The locale used when a tag matches no pool. */
76
+ export const defaultSampleLocale = 'en-GB';
77
+
78
+ /** The locale tags with their own pools. */
79
+ export const sampleLocaleTags: string[] = [...locales.keys()];
80
+
81
+ /**
82
+ * Resolves a BCP 47 tag to a pool: the exact tag (language lower case, region upper case), else the
83
+ * language's default pool, else {@link defaultSampleLocale}.
84
+ */
85
+ export function sampleLocaleFor(tag: string | undefined): SampleLocale {
86
+ const parts = (tag ?? '').trim().split('-');
87
+ const language = parts[0].toLowerCase();
88
+ const region = parts.length > 1 ? parts[1].toUpperCase() : '';
89
+ const exact = region === '' ? undefined : locales.get(`${language}-${region}`);
90
+ return exact ?? locales.get(languageDefaults.get(language) ?? defaultSampleLocale)!;
91
+ }
@@ -0,0 +1,48 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { SampleDataPack } from '@cratis/scene.model';
5
+ import { isValidSampleDataOwner, sampleDataIdPattern, sampleDataLocalePattern } from './sampleDataRules';
6
+
7
+ /**
8
+ * Checks a pack's schema: an id of the allowed form, a title, a well-formed locale tag, an owner that is
9
+ * `user` or `blueprint:<id>`, and sets with a shape each, none repeated. The C# engine
10
+ * (`SampleDataValidation.ValidatePack`) reports the same problems in the same order; both are asserted against
11
+ * `sample-data-resolution-fixtures.json`.
12
+ */
13
+ export function validateSampleDataPack(pack: SampleDataPack): string[] {
14
+ const problems: string[] = [];
15
+ const id = pack.id ?? '';
16
+ const label = id.trim() === '' ? 'Sample data pack' : `Sample data pack '${id}'`;
17
+
18
+ if (id.trim() === '') {
19
+ problems.push('Sample data pack declares no id');
20
+ } else if (!sampleDataIdPattern.test(id)) {
21
+ problems.push(`${label} declares an invalid id; use lower-case letters, digits and '.', '_' or '-' separators`);
22
+ }
23
+
24
+ if ((pack.title ?? '').trim() === '') problems.push(`${label} declares no title`);
25
+
26
+ if ((pack.locale ?? '').trim() === '') {
27
+ problems.push(`${label} declares no locale`);
28
+ } else if (!sampleDataLocalePattern.test(pack.locale)) {
29
+ problems.push(`${label} declares an invalid locale '${pack.locale}'`);
30
+ }
31
+
32
+ if (!isValidSampleDataOwner(pack.owner)) {
33
+ problems.push(`${label} declares owner '${pack.owner ?? ''}', which is neither 'user' nor 'blueprint:<id>'`);
34
+ }
35
+
36
+ const seen = new Set<string>();
37
+ for (const set of pack.sets ?? []) {
38
+ if ((set.shape ?? '').trim() === '') {
39
+ problems.push(`${label} declares a set without a shape`);
40
+ } else if (seen.has(set.shape)) {
41
+ problems.push(`${label} declares shape '${set.shape}' more than once`);
42
+ } else {
43
+ seen.add(set.shape);
44
+ }
45
+ }
46
+
47
+ return problems;
48
+ }
@@ -0,0 +1,79 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { PackageKind } from '@cratis/scene.model';
5
+ import { SampleDataPackSource } from './SampleDataPackSource';
6
+ import { isValidSampleDataOwner, isValidSampleDataPath, sampleDataIdPattern, sampleDataLocalePattern } from './sampleDataRules';
7
+ import { validateSampleDataPack } from './validateSampleDataPack';
8
+
9
+ /**
10
+ * Checks the sample data the given packages ship, in package then reference order: only a Blueprint package may
11
+ * ship packs; each reference needs a valid id, a title, a locale tag and a `sampleData/*.json` path, with no id
12
+ * or path repeated; and each loaded pack must be valid, match its reference's id and locale, be owned by a
13
+ * blueprint, and be one the manifest references. The C# engine (`SampleDataValidation.ValidatePackages`)
14
+ * reports the same problems in the same order.
15
+ */
16
+ export function validateSampleDataPackages(sources: readonly SampleDataPackSource[]): string[] {
17
+ const problems: string[] = [];
18
+
19
+ for (const source of sources) {
20
+ const manifest = source.manifest;
21
+ const references = manifest.sampleData ?? [];
22
+ const owner = `Package '${manifest.name}'`;
23
+
24
+ if (references.length > 0 && manifest.kind !== PackageKind.Blueprint) {
25
+ problems.push(`${owner} ships sample data but is not a Blueprint package`);
26
+ }
27
+
28
+ const ids = new Set<string>();
29
+ const paths = new Set<string>();
30
+ for (const reference of references) {
31
+ const label = `${owner} sample data pack '${reference.id ?? ''}'`;
32
+ if ((reference.id ?? '').trim() === '') {
33
+ problems.push(`${owner} declares a sample data pack without an id`);
34
+ } else if (!sampleDataIdPattern.test(reference.id)) {
35
+ problems.push(`${label} has an invalid id`);
36
+ } else if (ids.has(reference.id)) {
37
+ problems.push(`${label} is declared more than once`);
38
+ } else {
39
+ ids.add(reference.id);
40
+ }
41
+
42
+ if ((reference.title ?? '').trim() === '') problems.push(`${label} declares no title`);
43
+
44
+ if ((reference.locale ?? '').trim() === '') {
45
+ problems.push(`${label} declares no locale`);
46
+ } else if (!sampleDataLocalePattern.test(reference.locale)) {
47
+ problems.push(`${label} declares an invalid locale '${reference.locale}'`);
48
+ }
49
+
50
+ if (!isValidSampleDataPath(reference.path)) {
51
+ problems.push(`${label} declares path '${reference.path ?? ''}', which is not 'sampleData/<name>.json'`);
52
+ } else if (paths.has(reference.path)) {
53
+ problems.push(`${label} reuses the path '${reference.path}'`);
54
+ } else {
55
+ paths.add(reference.path);
56
+ }
57
+ }
58
+
59
+ for (const pack of source.packs ?? []) {
60
+ const label = `${owner} sample data pack '${pack.id ?? ''}'`;
61
+ problems.push(...validateSampleDataPack(pack));
62
+ const reference = references.find(candidate => candidate.id === pack.id);
63
+ if (reference === undefined) {
64
+ problems.push(`${label} is not referenced by the package's sampleData`);
65
+ continue;
66
+ }
67
+
68
+ if (reference.locale !== pack.locale) {
69
+ problems.push(`${label} has locale '${pack.locale}' but its reference declares '${reference.locale}'`);
70
+ }
71
+
72
+ if (isValidSampleDataOwner(pack.owner) && pack.owner === 'user') {
73
+ problems.push(`${label} must be owned by a blueprint, not 'user'`);
74
+ }
75
+ }
76
+ }
77
+
78
+ return problems;
79
+ }
@@ -0,0 +1,55 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { SampleDataPack, SampleDataSelection } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * Checks an application's sample data selection against the packs available: the active pack and every bound
8
+ * pack must exist, mappings and bindings must be complete and not repeated, and one read model must not be fed
9
+ * by two different shapes. The C# engine (`SampleDataValidation.ValidateSelection`) reports the same problems in
10
+ * the same order.
11
+ */
12
+ export function validateSampleDataSelection(selection: SampleDataSelection, packs: readonly SampleDataPack[]): string[] {
13
+ const problems: string[] = [];
14
+ const known = new Set(packs.map(pack => pack.id));
15
+
16
+ if (selection.activePack !== undefined && selection.activePack !== null && !known.has(selection.activePack)) {
17
+ problems.push(`The active sample data pack '${selection.activePack}' is not available`);
18
+ }
19
+
20
+ const pairs = new Set<string>();
21
+ const shapeByReadModel = new Map<string, string>();
22
+ for (const mapping of selection.mappings ?? []) {
23
+ if ((mapping.shape ?? '').trim() === '') {
24
+ problems.push('A sample data mapping declares no shape');
25
+ } else if ((mapping.readModel ?? '').trim() === '') {
26
+ problems.push(`The sample data mapping for shape '${mapping.shape}' declares no read model`);
27
+ } else if (pairs.has(`${mapping.shape}\u0000${mapping.readModel}`)) {
28
+ problems.push(`The sample data mapping '${mapping.shape}' to '${mapping.readModel}' is declared more than once`);
29
+ } else {
30
+ pairs.add(`${mapping.shape}\u0000${mapping.readModel}`);
31
+ const existing = shapeByReadModel.get(mapping.readModel);
32
+ if (existing === undefined) {
33
+ shapeByReadModel.set(mapping.readModel, mapping.shape);
34
+ } else {
35
+ problems.push(`The read model '${mapping.readModel}' is mapped from both shape '${existing}' and shape '${mapping.shape}'`);
36
+ }
37
+ }
38
+ }
39
+
40
+ const targets = new Set<string>();
41
+ for (const binding of selection.bindings ?? []) {
42
+ if ((binding.target ?? '').trim() === '') {
43
+ problems.push('A sample data binding declares no target');
44
+ } else if (targets.has(binding.target)) {
45
+ problems.push(`The sample data target '${binding.target}' is bound more than once`);
46
+ } else {
47
+ targets.add(binding.target);
48
+ if (!known.has(binding.pack)) {
49
+ problems.push(`The sample data binding for '${binding.target}' names pack '${binding.pack ?? ''}', which is not available`);
50
+ }
51
+ }
52
+ }
53
+
54
+ return problems;
55
+ }