@mkja/o-data 0.0.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.
@@ -0,0 +1,1157 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+ import * as fs from 'fs';
3
+ import path, { dirname } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+ function normalizeExcludeFilters(filters) {
6
+ const normalize = (patterns) => {
7
+ if (!patterns)
8
+ return [];
9
+ return patterns.map((p) => (typeof p === 'string' ? new RegExp(p) : p));
10
+ };
11
+ return {
12
+ entities: normalize(filters?.entities),
13
+ complexTypes: normalize(filters?.complexTypes),
14
+ actions: normalize(filters?.actions),
15
+ functions: normalize(filters?.functions),
16
+ properties: normalize(filters?.properties),
17
+ navigations: normalize(filters?.navigations),
18
+ };
19
+ }
20
+ function normalizeMaskRules(mask) {
21
+ const normalize = (patterns) => {
22
+ if (!patterns)
23
+ return [];
24
+ return patterns.map((p) => (typeof p === 'string' ? new RegExp(p) : p));
25
+ };
26
+ const normalizeByEntity = (input) => {
27
+ const result = new Map();
28
+ if (!input)
29
+ return result;
30
+ for (const [key, value] of Object.entries(input)) {
31
+ if (value === 'ALL') {
32
+ result.set(key, { all: true, patterns: [] });
33
+ }
34
+ else {
35
+ result.set(key, { all: false, patterns: normalize(value) });
36
+ }
37
+ }
38
+ return result;
39
+ };
40
+ const normalizeByEntityOnly = (input) => {
41
+ const result = new Map();
42
+ if (!input)
43
+ return result;
44
+ for (const [key, value] of Object.entries(input)) {
45
+ result.set(key, normalize(value));
46
+ }
47
+ return result;
48
+ };
49
+ return {
50
+ entities: normalize(mask?.entities),
51
+ boundActionsByEntity: normalizeByEntity(mask?.boundActionsByEntity),
52
+ boundFunctionsByEntity: normalizeByEntity(mask?.boundFunctionsByEntity),
53
+ unboundActions: normalize(mask?.unboundActions),
54
+ unboundFunctions: normalize(mask?.unboundFunctions),
55
+ onlyBoundActionsByEntity: normalizeByEntityOnly(mask?.onlyBoundActionsByEntity),
56
+ };
57
+ }
58
+ async function loadConfig(configPathArgFromCaller) {
59
+ const configPathArg = configPathArgFromCaller ?? process.argv[2];
60
+ let configPath = null;
61
+ // Check for config path in first CLI arg
62
+ if (configPathArg) {
63
+ const root = process.cwd();
64
+ configPath = path.isAbsolute(configPathArg) ? configPathArg : path.join(root, configPathArg);
65
+ if (!fs.existsSync(configPath)) {
66
+ throw new Error(`Config file not found: ${configPath}`);
67
+ }
68
+ }
69
+ else {
70
+ // Look for default config file in cwd (prefer JS for Node compatibility)
71
+ const root = process.cwd();
72
+ const jsConfigPath = path.join(root, 'odata-parser.config.js');
73
+ const tsConfigPath = path.join(root, 'odata-parser.config.ts');
74
+ if (fs.existsSync(jsConfigPath)) {
75
+ configPath = jsConfigPath;
76
+ }
77
+ else if (fs.existsSync(tsConfigPath)) {
78
+ // TS config is supported primarily for local Bun-based development
79
+ configPath = tsConfigPath;
80
+ }
81
+ }
82
+ // Config file is required
83
+ if (!configPath) {
84
+ throw new Error('Config file not found. Either provide a path or create odata-parser.config.js in the current directory.');
85
+ }
86
+ // Load config
87
+ try {
88
+ const configUrl = pathToFileURL(configPath).href;
89
+ const configModule = await import(configUrl);
90
+ const config = configModule.default || configModule;
91
+ if (!config.inputPath || !config.outputPath) {
92
+ throw new Error('Config must specify inputPath and outputPath');
93
+ }
94
+ const configDir = path.dirname(configPath);
95
+ const inputFile = path.resolve(configDir, config.inputPath);
96
+ const outputFile = path.resolve(configDir, config.outputPath, 'generated-o-data-schema.ts');
97
+ return {
98
+ inputFile,
99
+ outputFile,
100
+ wantedEntities: config.wantedEntities || [],
101
+ wantedUnboundActions: config.wantedUnboundActions,
102
+ wantedUnboundFunctions: config.wantedUnboundFunctions,
103
+ excludeFilters: normalizeExcludeFilters(config.excludeFilters),
104
+ selectionMode: config.selectionMode ?? 'additive',
105
+ onlyEntities: config.onlyEntities,
106
+ onlyBoundActions: config.onlyBoundActions,
107
+ onlyBoundFunctions: config.onlyBoundFunctions,
108
+ onlyUnboundActions: config.onlyUnboundActions,
109
+ onlyUnboundFunctions: config.onlyUnboundFunctions,
110
+ mask: normalizeMaskRules(config.mask),
111
+ };
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Error loading config file: ${String(error)}`);
115
+ }
116
+ }
117
+ // ----------------------------------------------------------------------------
118
+ // Main Conversion Logic
119
+ // ----------------------------------------------------------------------------
120
+ export async function generateSchema(configPath) {
121
+ // Load configuration
122
+ const config = await loadConfig(configPath);
123
+ const INPUT_FILE = config.inputFile;
124
+ const OUTPUT_FILE = config.outputFile;
125
+ const WANTED_ENTITIES = config.wantedEntities;
126
+ const WANTED_UNBOUND_ACTIONS = config.wantedUnboundActions;
127
+ const WANTED_UNBOUND_FUNCTIONS = config.wantedUnboundFunctions;
128
+ const EXCLUDE_FILTERS = config.excludeFilters;
129
+ const SELECTION_MODE = config.selectionMode;
130
+ const ONLY_ENTITIES = config.onlyEntities;
131
+ const ONLY_BOUND_ACTIONS = config.onlyBoundActions;
132
+ const ONLY_BOUND_FUNCTIONS = config.onlyBoundFunctions;
133
+ const ONLY_UNBOUND_ACTIONS = config.onlyUnboundActions;
134
+ const ONLY_UNBOUND_FUNCTIONS = config.onlyUnboundFunctions;
135
+ const MASK = config.mask;
136
+ if (!fs.existsSync(INPUT_FILE)) {
137
+ throw new Error(`Input file not found: ${INPUT_FILE}`);
138
+ }
139
+ const xmlData = fs.readFileSync(INPUT_FILE, 'utf-8');
140
+ const parser = new XMLParser({
141
+ ignoreAttributes: false,
142
+ attributeNamePrefix: '@_',
143
+ isArray: (name) => {
144
+ const arrayTags = [
145
+ 'Property',
146
+ 'NavigationProperty',
147
+ 'NavigationPropertyBinding',
148
+ 'EntitySet',
149
+ 'EntityType',
150
+ 'ComplexType',
151
+ 'EnumType',
152
+ 'Action',
153
+ 'Function',
154
+ 'Parameter',
155
+ 'PropertyRef',
156
+ 'FunctionImport',
157
+ 'ActionImport',
158
+ 'Member',
159
+ ];
160
+ return arrayTags.includes(name);
161
+ },
162
+ });
163
+ const parsed = parser.parse(xmlData);
164
+ const edmx = parsed['edmx:Edmx'] || parsed.Edmx;
165
+ const dataServices = edmx['edmx:DataServices'] || edmx.DataServices;
166
+ const schemas = Array.isArray(dataServices.Schema)
167
+ ? dataServices.Schema
168
+ : [dataServices.Schema];
169
+ const mainSchema = schemas.find((s) => s.EntityType && s.EntityType.length > 0) || schemas[0];
170
+ if (!mainSchema) {
171
+ throw new Error('No schema found in CSDL document');
172
+ }
173
+ const namespace = mainSchema['@_Namespace'];
174
+ const alias = mainSchema['@_Alias'] || '';
175
+ // --------------------------------------------------------------------------
176
+ // HELPER: Type Resolution (Handles Collections & Aliases)
177
+ // --------------------------------------------------------------------------
178
+ function resolveType(rawType) {
179
+ let isCollection = false;
180
+ let clean = rawType || '';
181
+ if (clean.startsWith('Collection(')) {
182
+ isCollection = true;
183
+ clean = clean.match(/Collection\((.*?)\)/)?.[1] || clean;
184
+ }
185
+ // Resolve Alias (e.g. mscrm.incidentresolution -> Microsoft.Dynamics.CRM.incidentresolution)
186
+ if (alias && clean.startsWith(alias + '.')) {
187
+ clean = clean.replace(alias + '.', namespace + '.');
188
+ }
189
+ return { name: clean, isCollection, original: rawType };
190
+ }
191
+ // --------------------------------------------------------------------------
192
+ // HELPER: Exclusion Check
193
+ // --------------------------------------------------------------------------
194
+ function isExcluded(name, category) {
195
+ return EXCLUDE_FILTERS[category].some((r) => r.test(name));
196
+ }
197
+ // --------------------------------------------------------------------------
198
+ // Phase 0: Indexing Everything
199
+ // --------------------------------------------------------------------------
200
+ const typeToSetMap = new Map(); // EntityType FQN -> EntitySet Name
201
+ const setToTypeMap = new Map(); // EntitySet name -> EntityType FQN
202
+ const entityTypes = new Map(); // FQN -> EntityType Definition
203
+ const complexTypes = new Map(); // FQN -> ComplexType Definition
204
+ const enumTypes = new Map(); // FQN -> EnumType Definition
205
+ for (const s of schemas) {
206
+ const ns = s['@_Namespace'];
207
+ if (s.EntityType) {
208
+ for (const et of s.EntityType)
209
+ entityTypes.set(`${ns}.${et['@_Name']}`, et);
210
+ }
211
+ if (s.EnumType) {
212
+ for (const et of s.EnumType) {
213
+ const fqn = `${ns}.${et['@_Name']}`;
214
+ enumTypes.set(fqn, et);
215
+ }
216
+ }
217
+ if (s.ComplexType) {
218
+ for (const ct of s.ComplexType)
219
+ complexTypes.set(`${ns}.${ct['@_Name']}`, ct);
220
+ }
221
+ }
222
+ let entityContainer = mainSchema.EntityContainer;
223
+ if (!entityContainer) {
224
+ const containerSchema = schemas.find((s) => s.EntityContainer);
225
+ if (containerSchema)
226
+ entityContainer = containerSchema.EntityContainer;
227
+ }
228
+ if (entityContainer && entityContainer.EntitySet) {
229
+ for (const set of entityContainer.EntitySet) {
230
+ const setName = set['@_Name'];
231
+ const typeFqn = set['@_EntityType'];
232
+ typeToSetMap.set(typeFqn, setName);
233
+ setToTypeMap.set(setName, typeFqn);
234
+ }
235
+ }
236
+ // Parse FunctionImport and ActionImport for import tracking
237
+ const functionImports = new Map(); // ImportName -> FunctionFQN
238
+ const actionImports = new Map(); // ImportName -> ActionFQN
239
+ if (entityContainer) {
240
+ // Parse FunctionImports
241
+ if (entityContainer.FunctionImport) {
242
+ for (const fi of entityContainer.FunctionImport) {
243
+ const functionFqn = fi['@_Function'];
244
+ const { name: resolvedFqn } = resolveType(functionFqn);
245
+ functionImports.set(fi['@_Name'], resolvedFqn);
246
+ }
247
+ }
248
+ // Parse ActionImports
249
+ if (entityContainer.ActionImport) {
250
+ for (const ai of entityContainer.ActionImport) {
251
+ const actionFqn = ai['@_Action'];
252
+ const { name: resolvedFqn } = resolveType(actionFqn);
253
+ actionImports.set(ai['@_Name'], resolvedFqn);
254
+ }
255
+ }
256
+ }
257
+ // --------------------------------------------------------------------------
258
+ // Phase 1: Core Schema Discovery
259
+ // --------------------------------------------------------------------------
260
+ // 1.1 EntitySet Discovery
261
+ const includedEntitySets = new Set();
262
+ const operationExpandedEntitySets = new Set();
263
+ const operationExpandedEntityTypes = new Set();
264
+ if (WANTED_ENTITIES === 'ALL') {
265
+ if (entityContainer && entityContainer.EntitySet) {
266
+ for (const set of entityContainer.EntitySet) {
267
+ if (!isExcluded(set['@_Name'], 'entities')) {
268
+ includedEntitySets.add(set['@_Name']);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ else {
274
+ for (const setName of WANTED_ENTITIES) {
275
+ if (!isExcluded(setName, 'entities')) {
276
+ includedEntitySets.add(setName);
277
+ }
278
+ }
279
+ }
280
+ // 1.2 EntityType Discovery (including baseType chain)
281
+ const includedEntityTypes = new Set(); // EntityType FQNs
282
+ function resolveBaseTypeChain(entityTypeFQN, visited = new Set()) {
283
+ if (visited.has(entityTypeFQN))
284
+ return; // Prevent circular references
285
+ visited.add(entityTypeFQN);
286
+ const entityType = entityTypes.get(entityTypeFQN);
287
+ if (!entityType)
288
+ return;
289
+ includedEntityTypes.add(entityTypeFQN);
290
+ if (entityType['@_BaseType']) {
291
+ const { name: baseTypeFQN } = resolveType(entityType['@_BaseType']);
292
+ resolveBaseTypeChain(baseTypeFQN, visited);
293
+ }
294
+ }
295
+ // Add EntityTypes for included EntitySets
296
+ for (const setName of includedEntitySets) {
297
+ const typeFqn = setToTypeMap.get(setName);
298
+ if (typeFqn) {
299
+ resolveBaseTypeChain(typeFqn);
300
+ }
301
+ }
302
+ // 1.3 Property and Navigation Extraction
303
+ const includedComplexTypes = new Set();
304
+ const includedEnumTypes = new Set();
305
+ function extractTypeDependencies(typeFQN, isCollection, options) {
306
+ const { name: resolvedType } = resolveType(typeFQN);
307
+ if (resolvedType.startsWith('Edm.')) {
308
+ return; // Primitive type
309
+ }
310
+ if (enumTypes.has(resolvedType)) {
311
+ if (!isExcluded(resolvedType, 'complexTypes')) {
312
+ includedEnumTypes.add(resolvedType);
313
+ }
314
+ return;
315
+ }
316
+ if (complexTypes.has(resolvedType)) {
317
+ if (!isExcluded(resolvedType, 'complexTypes')) {
318
+ includedComplexTypes.add(resolvedType);
319
+ // Recursively extract dependencies from complex type properties
320
+ const ct = complexTypes.get(resolvedType);
321
+ if (ct && ct.Property) {
322
+ for (const prop of ct.Property) {
323
+ extractTypeDependencies(prop['@_Type'], false, options);
324
+ }
325
+ }
326
+ }
327
+ return;
328
+ }
329
+ // Check if EntityType
330
+ if (entityTypes.has(resolvedType)) {
331
+ const entitySetName = typeToSetMap.get(resolvedType);
332
+ if (options?.allowEntitySetExpansionFromEntityType) {
333
+ // Operation-based or explicit expansion: allow new entity sets
334
+ if (entitySetName && !isExcluded(entitySetName, 'entities')) {
335
+ if (!includedEntitySets.has(entitySetName)) {
336
+ includedEntitySets.add(entitySetName);
337
+ operationExpandedEntitySets.add(entitySetName);
338
+ operationExpandedEntityTypes.add(resolvedType);
339
+ }
340
+ if (!includedEntityTypes.has(resolvedType)) {
341
+ resolveBaseTypeChain(resolvedType);
342
+ }
343
+ }
344
+ }
345
+ else {
346
+ // Structural/entity-set–driven paths: respect wantedEntities whitelist
347
+ if (entitySetName && includedEntitySets.has(entitySetName)) {
348
+ if (!includedEntityTypes.has(resolvedType)) {
349
+ resolveBaseTypeChain(resolvedType);
350
+ }
351
+ }
352
+ }
353
+ return;
354
+ }
355
+ }
356
+ // Extract properties and navigations from included EntityTypes
357
+ for (const entityTypeFQN of includedEntityTypes) {
358
+ const entityType = entityTypes.get(entityTypeFQN);
359
+ if (!entityType)
360
+ continue;
361
+ // Extract regular properties
362
+ if (entityType.Property) {
363
+ for (const prop of entityType.Property) {
364
+ if (isExcluded(prop['@_Name'], 'properties'))
365
+ continue;
366
+ extractTypeDependencies(prop['@_Type'], false, { allowEntitySetExpansionFromEntityType: false });
367
+ }
368
+ }
369
+ // Extract navigation properties (only if target is included)
370
+ if (entityType.NavigationProperty) {
371
+ for (const nav of entityType.NavigationProperty) {
372
+ if (isExcluded(nav['@_Name'], 'navigations'))
373
+ continue;
374
+ const { name: navTargetFQN } = resolveType(nav['@_Type']);
375
+ // Only include navigation if target EntityType is included
376
+ if (includedEntityTypes.has(navTargetFQN)) {
377
+ // Navigation is included, no additional dependencies
378
+ }
379
+ }
380
+ }
381
+ }
382
+ // 1.4 Resolve Complex/Enum Dependencies Recursively
383
+ function resolveComplexDependencies() {
384
+ let changed = true;
385
+ while (changed) {
386
+ changed = false;
387
+ for (const ctFqn of includedComplexTypes) {
388
+ const ct = complexTypes.get(ctFqn);
389
+ if (!ct || !ct.Property)
390
+ continue;
391
+ for (const prop of ct.Property) {
392
+ const { name: propType } = resolveType(prop['@_Type']);
393
+ if (propType.startsWith('Edm.'))
394
+ continue;
395
+ if (enumTypes.has(propType)) {
396
+ if (!includedEnumTypes.has(propType) && !isExcluded(propType, 'complexTypes')) {
397
+ includedEnumTypes.add(propType);
398
+ changed = true;
399
+ }
400
+ }
401
+ else if (complexTypes.has(propType)) {
402
+ if (!includedComplexTypes.has(propType) && !isExcluded(propType, 'complexTypes')) {
403
+ includedComplexTypes.add(propType);
404
+ changed = true;
405
+ }
406
+ }
407
+ }
408
+ }
409
+ }
410
+ }
411
+ resolveComplexDependencies();
412
+ // --------------------------------------------------------------------------
413
+ // Phase 2: Operations Discovery
414
+ // --------------------------------------------------------------------------
415
+ const boundOperations = new Map();
416
+ const unboundActions = [];
417
+ const unboundFunctions = [];
418
+ // Helper to check if operation should be included
419
+ function shouldIncludeUnboundOperation(op, opType) {
420
+ const name = op['@_Name'];
421
+ const category = opType === 'Action' ? 'actions' : 'functions';
422
+ const wantedList = opType === 'Action' ? WANTED_UNBOUND_ACTIONS : WANTED_UNBOUND_FUNCTIONS;
423
+ if (isExcluded(name, category)) {
424
+ return false;
425
+ }
426
+ if (wantedList === 'ALL') {
427
+ return true;
428
+ }
429
+ if (wantedList && Array.isArray(wantedList)) {
430
+ return wantedList.includes(name);
431
+ }
432
+ return false;
433
+ }
434
+ // Helper to register complex/enum dependencies from operations
435
+ function registerOperationDependencies(op) {
436
+ if (op.Parameter) {
437
+ for (const param of op.Parameter) {
438
+ extractTypeDependencies(param['@_Type'], false, { allowEntitySetExpansionFromEntityType: true });
439
+ }
440
+ }
441
+ if (op.ReturnType) {
442
+ extractTypeDependencies(op.ReturnType['@_Type'], false, { allowEntitySetExpansionFromEntityType: true });
443
+ }
444
+ }
445
+ // Process Actions
446
+ if (mainSchema.Action) {
447
+ for (const op of mainSchema.Action) {
448
+ const isBound = op['@_IsBound'] === 'true';
449
+ if (isBound) {
450
+ // Bound action - include if bound to included EntityType
451
+ if (op.Parameter && op.Parameter.length > 0) {
452
+ const bindingParam = op.Parameter[0];
453
+ if (!bindingParam)
454
+ continue;
455
+ const { name: bindingTypeFQN, isCollection } = resolveType(bindingParam['@_Type']);
456
+ const bindingSetName = typeToSetMap.get(bindingTypeFQN);
457
+ let isBindingEntityAllowed = true;
458
+ if (WANTED_ENTITIES !== 'ALL') {
459
+ if (!bindingSetName || !WANTED_ENTITIES.includes(bindingSetName)) {
460
+ isBindingEntityAllowed = false;
461
+ }
462
+ }
463
+ if (isBindingEntityAllowed && includedEntityTypes.has(bindingTypeFQN) && !isExcluded(op['@_Name'], 'actions')) {
464
+ registerOperationDependencies(op);
465
+ const processed = {
466
+ def: op,
467
+ type: 'Action',
468
+ isBound: true,
469
+ bindingTypeFQN,
470
+ isCollectionBound: isCollection,
471
+ };
472
+ if (!boundOperations.has(bindingTypeFQN)) {
473
+ boundOperations.set(bindingTypeFQN, { actions: [], functions: [] });
474
+ }
475
+ boundOperations.get(bindingTypeFQN).actions.push(processed);
476
+ }
477
+ }
478
+ }
479
+ else {
480
+ // Unbound action
481
+ if (shouldIncludeUnboundOperation(op, 'Action')) {
482
+ registerOperationDependencies(op);
483
+ unboundActions.push({
484
+ def: op,
485
+ type: 'Action',
486
+ isBound: false,
487
+ });
488
+ }
489
+ }
490
+ }
491
+ }
492
+ // Process Functions
493
+ if (mainSchema.Function) {
494
+ for (const op of mainSchema.Function) {
495
+ const isBound = op['@_IsBound'] === 'true';
496
+ if (isBound) {
497
+ // Bound function - include if bound to included EntityType
498
+ if (op.Parameter && op.Parameter.length > 0) {
499
+ const bindingParam = op.Parameter[0];
500
+ if (!bindingParam)
501
+ continue;
502
+ const { name: bindingTypeFQN, isCollection } = resolveType(bindingParam['@_Type']);
503
+ const bindingSetName = typeToSetMap.get(bindingTypeFQN);
504
+ let isBindingEntityAllowed = true;
505
+ if (WANTED_ENTITIES !== 'ALL') {
506
+ if (!bindingSetName || !WANTED_ENTITIES.includes(bindingSetName)) {
507
+ isBindingEntityAllowed = false;
508
+ }
509
+ }
510
+ if (isBindingEntityAllowed && includedEntityTypes.has(bindingTypeFQN) && !isExcluded(op['@_Name'], 'functions')) {
511
+ registerOperationDependencies(op);
512
+ const processed = {
513
+ def: op,
514
+ type: 'Function',
515
+ isBound: true,
516
+ bindingTypeFQN,
517
+ isCollectionBound: isCollection,
518
+ };
519
+ if (!boundOperations.has(bindingTypeFQN)) {
520
+ boundOperations.set(bindingTypeFQN, { actions: [], functions: [] });
521
+ }
522
+ boundOperations.get(bindingTypeFQN).functions.push(processed);
523
+ }
524
+ }
525
+ }
526
+ else {
527
+ // Unbound function
528
+ if (shouldIncludeUnboundOperation(op, 'Function')) {
529
+ registerOperationDependencies(op);
530
+ unboundFunctions.push({
531
+ def: op,
532
+ type: 'Function',
533
+ isBound: false,
534
+ });
535
+ }
536
+ }
537
+ }
538
+ }
539
+ // Resolve dependencies again after operations
540
+ resolveComplexDependencies();
541
+ // Additional sweep: Re-extract dependencies from all included EntityTypes
542
+ // This ensures we capture complex types that might have been missed
543
+ // (e.g., complex types only referenced in properties of EntityTypes
544
+ // that were added during operation dependency resolution)
545
+ for (const entityTypeFQN of includedEntityTypes) {
546
+ const entityType = entityTypes.get(entityTypeFQN);
547
+ if (!entityType)
548
+ continue;
549
+ // Extract dependencies from regular properties
550
+ if (entityType.Property) {
551
+ for (const prop of entityType.Property) {
552
+ if (isExcluded(prop['@_Name'], 'properties'))
553
+ continue;
554
+ extractTypeDependencies(prop['@_Type'], false, { allowEntitySetExpansionFromEntityType: false });
555
+ }
556
+ }
557
+ // Note: Navigation properties don't typically have complex type dependencies
558
+ // but we've already processed them in Phase 1
559
+ }
560
+ // Final dependency resolution after the sweep
561
+ resolveComplexDependencies();
562
+ // Apply selection-mode and mask rules before code generation
563
+ applyOnlyModeFilters();
564
+ applyMaskRules();
565
+ pruneOperationExpandedEntities();
566
+ // --------------------------------------------------------------------------
567
+ // Phase 3: Code Generation
568
+ // --------------------------------------------------------------------------
569
+ // Helper to get short name from FQN
570
+ function getShortName(fqn) {
571
+ return fqn.split('.').pop();
572
+ }
573
+ // Helper to generate property code
574
+ function generatePropertyCode(prop, key) {
575
+ const propName = prop['@_Name'];
576
+ if (propName.startsWith('_'))
577
+ return '';
578
+ const { name: resolvedType, isCollection } = resolveType(prop['@_Type']);
579
+ const nullable = prop['@_Nullable'] !== 'false';
580
+ // Check if enum
581
+ if (enumTypes.has(resolvedType)) {
582
+ const shortName = getShortName(resolvedType);
583
+ const options = [];
584
+ if (isCollection)
585
+ options.push('collection: true');
586
+ if (!nullable)
587
+ options.push('nullable: false');
588
+ if (options.length > 0) {
589
+ return ` "${propName}": { type: 'enum', target: '${shortName}', ${options.join(', ')} },\n`;
590
+ }
591
+ return ` "${propName}": { type: 'enum', target: '${shortName}' },\n`;
592
+ }
593
+ // Check if complex type
594
+ if (complexTypes.has(resolvedType)) {
595
+ const shortName = getShortName(resolvedType);
596
+ const options = [];
597
+ if (isCollection)
598
+ options.push('collection: true');
599
+ if (!nullable)
600
+ options.push('nullable: false');
601
+ if (options.length > 0) {
602
+ return ` "${propName}": { type: 'complex', target: '${shortName}', ${options.join(', ')} },\n`;
603
+ }
604
+ return ` "${propName}": { type: 'complex', target: '${shortName}' },\n`;
605
+ }
606
+ // Check if EntityType (navigation)
607
+ if (includedEntityTypes.has(resolvedType)) {
608
+ const shortName = getShortName(resolvedType);
609
+ const options = [];
610
+ if (isCollection)
611
+ options.push('collection: true');
612
+ if (!nullable)
613
+ options.push('nullable: false');
614
+ if (options.length > 0) {
615
+ return ` "${propName}": { type: 'navigation', target: '${shortName}', ${options.join(', ')} },\n`;
616
+ }
617
+ return ` "${propName}": { type: 'navigation', target: '${shortName}' },\n`;
618
+ }
619
+ // Primitive type
620
+ const edmType = resolvedType.startsWith('Edm.') ? resolvedType : `Edm.${resolvedType}`;
621
+ const options = [];
622
+ if (isCollection)
623
+ options.push('collection: true');
624
+ if (!nullable)
625
+ options.push('nullable: false');
626
+ if (options.length > 0) {
627
+ return ` "${propName}": { type: '${edmType}', ${options.join(', ')} },\n`;
628
+ }
629
+ return ` "${propName}": { type: '${edmType}' },\n`;
630
+ }
631
+ // Helper to generate navigation code
632
+ function generateNavigationCode(nav) {
633
+ const navName = nav['@_Name'];
634
+ const { name: navTargetFQN, isCollection } = resolveType(nav['@_Type']);
635
+ if (!includedEntityTypes.has(navTargetFQN)) {
636
+ return ''; // Skip navigation if target not included
637
+ }
638
+ const targetShortName = getShortName(navTargetFQN);
639
+ return ` "${navName}": { type: 'navigation', target: '${targetShortName}', collection: ${isCollection} },\n`;
640
+ }
641
+ // Helper to generate parameter/return type code
642
+ function generateTypeCode(type) {
643
+ const { name: resolvedType, isCollection } = resolveType(type);
644
+ // Check if enum
645
+ if (enumTypes.has(resolvedType)) {
646
+ const shortName = getShortName(resolvedType);
647
+ if (isCollection) {
648
+ return `{ type: 'enum', target: '${shortName}', collection: true }`;
649
+ }
650
+ return `{ type: 'enum', target: '${shortName}' }`;
651
+ }
652
+ // Check if complex type
653
+ if (complexTypes.has(resolvedType)) {
654
+ const shortName = getShortName(resolvedType);
655
+ if (isCollection) {
656
+ return `{ type: 'complex', target: '${shortName}', collection: true }`;
657
+ }
658
+ return `{ type: 'complex', target: '${shortName}' }`;
659
+ }
660
+ // Check if EntityType
661
+ if (includedEntityTypes.has(resolvedType)) {
662
+ const shortName = getShortName(resolvedType);
663
+ if (isCollection) {
664
+ return `{ type: 'navigation', target: '${shortName}', collection: true }`;
665
+ }
666
+ return `{ type: 'navigation', target: '${shortName}' }`;
667
+ }
668
+ // Primitive type
669
+ const edmType = resolvedType.startsWith('Edm.') ? resolvedType : `Edm.${resolvedType}`;
670
+ if (isCollection) {
671
+ return `{ type: '${edmType}', collection: true }`;
672
+ }
673
+ return `{ type: '${edmType}' }`;
674
+ }
675
+ // ------------------------------------------------------------------------
676
+ // Phase 2.5: Apply selection-mode and mask rules
677
+ // ------------------------------------------------------------------------
678
+ function applyOnlyModeFilters() {
679
+ if (SELECTION_MODE !== 'only') {
680
+ return;
681
+ }
682
+ // Entities: intersect with ONLY_ENTITIES (by set name or short type name)
683
+ if (ONLY_ENTITIES && ONLY_ENTITIES.length > 0) {
684
+ const allowed = new Set(ONLY_ENTITIES);
685
+ // Filter entity sets
686
+ for (const setName of Array.from(includedEntitySets)) {
687
+ const typeFqn = setToTypeMap.get(setName);
688
+ const typeShort = typeFqn ? getShortName(typeFqn) : undefined;
689
+ if (!allowed.has(setName) && (!typeShort || !allowed.has(typeShort))) {
690
+ includedEntitySets.delete(setName);
691
+ }
692
+ }
693
+ // Filter entity types to those whose set (or short name) is allowed
694
+ for (const typeFqn of Array.from(includedEntityTypes)) {
695
+ const shortName = getShortName(typeFqn);
696
+ const setName = typeToSetMap.get(typeFqn);
697
+ if (!setName) {
698
+ // Entity types without a set are only kept if explicitly allowed by short name
699
+ if (!allowed.has(shortName)) {
700
+ includedEntityTypes.delete(typeFqn);
701
+ }
702
+ }
703
+ else if (!allowed.has(setName) && !allowed.has(shortName)) {
704
+ includedEntityTypes.delete(typeFqn);
705
+ }
706
+ }
707
+ }
708
+ // Bound operations: keep only those explicitly allowed if lists are provided
709
+ if ((ONLY_BOUND_ACTIONS && ONLY_BOUND_ACTIONS.length > 0) ||
710
+ (ONLY_BOUND_FUNCTIONS && ONLY_BOUND_FUNCTIONS.length > 0)) {
711
+ const allowedActions = new Set(ONLY_BOUND_ACTIONS ?? []);
712
+ const allowedFunctions = new Set(ONLY_BOUND_FUNCTIONS ?? []);
713
+ for (const [bindingTypeFQN, ops] of Array.from(boundOperations.entries())) {
714
+ ops.actions = ops.actions.filter(op => allowedActions.size === 0 || allowedActions.has(op.def['@_Name']));
715
+ ops.functions = ops.functions.filter(op => allowedFunctions.size === 0 || allowedFunctions.has(op.def['@_Name']));
716
+ if (ops.actions.length === 0 && ops.functions.length === 0) {
717
+ boundOperations.delete(bindingTypeFQN);
718
+ }
719
+ }
720
+ }
721
+ // Unbound operations: keep only those explicitly allowed
722
+ if (ONLY_UNBOUND_ACTIONS && ONLY_UNBOUND_ACTIONS.length > 0) {
723
+ const allowed = new Set(ONLY_UNBOUND_ACTIONS);
724
+ for (let i = unboundActions.length - 1; i >= 0; i--) {
725
+ const op = unboundActions[i];
726
+ if (!op)
727
+ continue;
728
+ if (!allowed.has(op.def['@_Name'])) {
729
+ unboundActions.splice(i, 1);
730
+ }
731
+ }
732
+ }
733
+ if (ONLY_UNBOUND_FUNCTIONS && ONLY_UNBOUND_FUNCTIONS.length > 0) {
734
+ const allowed = new Set(ONLY_UNBOUND_FUNCTIONS);
735
+ for (let i = unboundFunctions.length - 1; i >= 0; i--) {
736
+ const op = unboundFunctions[i];
737
+ if (!op)
738
+ continue;
739
+ if (!allowed.has(op.def['@_Name'])) {
740
+ unboundFunctions.splice(i, 1);
741
+ }
742
+ }
743
+ }
744
+ }
745
+ function applyMaskRules() {
746
+ const mask = MASK;
747
+ const getEntityKeysForBinding = (typeFqn) => {
748
+ const shortName = getShortName(typeFqn);
749
+ const setName = typeToSetMap.get(typeFqn);
750
+ const keys = new Set();
751
+ keys.add(shortName);
752
+ keys.add(typeFqn);
753
+ if (setName)
754
+ keys.add(setName);
755
+ return Array.from(keys);
756
+ };
757
+ // Helper: entity mask
758
+ const isEntityMasked = (setOrTypeName) => {
759
+ return mask.entities.some((r) => r.test(setOrTypeName));
760
+ };
761
+ // Helper: bound operation mask
762
+ const isBoundOperationMasked = (op) => {
763
+ if (!op.bindingTypeFQN)
764
+ return false;
765
+ const typeFqn = op.bindingTypeFQN;
766
+ const shortName = getShortName(typeFqn);
767
+ const setName = typeToSetMap.get(typeFqn);
768
+ const candidateKeys = new Set();
769
+ candidateKeys.add(shortName);
770
+ if (setName)
771
+ candidateKeys.add(setName);
772
+ candidateKeys.add(typeFqn);
773
+ const rulesMaps = op.type === 'Action' ? mask.boundActionsByEntity : mask.boundFunctionsByEntity;
774
+ for (const key of candidateKeys) {
775
+ const rule = rulesMaps.get(key);
776
+ if (!rule)
777
+ continue;
778
+ if (rule.all)
779
+ return true;
780
+ if (rule.patterns.some((r) => r.test(op.def['@_Name']))) {
781
+ return true;
782
+ }
783
+ }
784
+ return false;
785
+ };
786
+ // Helper: per-entity only-bound-actions whitelist
787
+ const shouldKeepBoundActionByOnlyList = (op) => {
788
+ if (!op.bindingTypeFQN)
789
+ return true;
790
+ if (mask.onlyBoundActionsByEntity.size === 0)
791
+ return true;
792
+ const keys = getEntityKeysForBinding(op.bindingTypeFQN);
793
+ const name = op.def['@_Name'];
794
+ let hasRule = false;
795
+ for (const key of keys) {
796
+ const patterns = mask.onlyBoundActionsByEntity.get(key);
797
+ if (!patterns)
798
+ continue;
799
+ hasRule = true;
800
+ if (patterns.some((r) => r.test(name))) {
801
+ return true;
802
+ }
803
+ }
804
+ if (hasRule)
805
+ return false;
806
+ return true;
807
+ };
808
+ // Helper: unbound operation mask
809
+ const isUnboundOperationMasked = (op) => {
810
+ const patterns = op.type === 'Action' ? mask.unboundActions : mask.unboundFunctions;
811
+ return patterns.some((r) => r.test(op.def['@_Name']));
812
+ };
813
+ // Mask entities (entity sets and types)
814
+ for (const setName of Array.from(includedEntitySets)) {
815
+ if (isEntityMasked(setName)) {
816
+ includedEntitySets.delete(setName);
817
+ const typeFqn = setToTypeMap.get(setName);
818
+ if (typeFqn) {
819
+ includedEntityTypes.delete(typeFqn);
820
+ }
821
+ }
822
+ }
823
+ for (const typeFqn of Array.from(includedEntityTypes)) {
824
+ const shortName = getShortName(typeFqn);
825
+ if (isEntityMasked(shortName) || isEntityMasked(typeFqn)) {
826
+ includedEntityTypes.delete(typeFqn);
827
+ const setName = typeToSetMap.get(typeFqn);
828
+ if (setName) {
829
+ includedEntitySets.delete(setName);
830
+ }
831
+ }
832
+ }
833
+ // Mask bound operations
834
+ for (const [bindingTypeFQN, ops] of Array.from(boundOperations.entries())) {
835
+ // Apply per-entity only-bound-actions whitelist first
836
+ ops.actions = ops.actions.filter((op) => shouldKeepBoundActionByOnlyList(op));
837
+ // Then apply negative masks
838
+ ops.actions = ops.actions.filter((op) => !isBoundOperationMasked(op));
839
+ ops.functions = ops.functions.filter((op) => !isBoundOperationMasked(op));
840
+ if (ops.actions.length === 0 && ops.functions.length === 0) {
841
+ boundOperations.delete(bindingTypeFQN);
842
+ }
843
+ }
844
+ // Mask unbound operations
845
+ for (let i = unboundActions.length - 1; i >= 0; i--) {
846
+ const op = unboundActions[i];
847
+ if (!op)
848
+ continue;
849
+ if (isUnboundOperationMasked(op)) {
850
+ unboundActions.splice(i, 1);
851
+ }
852
+ }
853
+ for (let i = unboundFunctions.length - 1; i >= 0; i--) {
854
+ const op = unboundFunctions[i];
855
+ if (!op)
856
+ continue;
857
+ if (isUnboundOperationMasked(op)) {
858
+ unboundFunctions.splice(i, 1);
859
+ }
860
+ }
861
+ }
862
+ function pruneOperationExpandedEntities() {
863
+ if (WANTED_ENTITIES === 'ALL') {
864
+ return;
865
+ }
866
+ const wantedSet = new Set(WANTED_ENTITIES);
867
+ const isEntityTypeReferencedByOperations = (entityTypeFQN) => {
868
+ const direct = boundOperations.get(entityTypeFQN);
869
+ if (direct && (direct.actions.length > 0 || direct.functions.length > 0)) {
870
+ return true;
871
+ }
872
+ const checkOpTypes = (op) => {
873
+ const def = op.def;
874
+ if (def.Parameter) {
875
+ for (const param of def.Parameter) {
876
+ const { name: t } = resolveType(param['@_Type']);
877
+ if (t === entityTypeFQN)
878
+ return true;
879
+ }
880
+ }
881
+ if (def.ReturnType) {
882
+ const { name: t } = resolveType(def.ReturnType['@_Type']);
883
+ if (t === entityTypeFQN)
884
+ return true;
885
+ }
886
+ return false;
887
+ };
888
+ for (const [, ops] of boundOperations) {
889
+ for (const op of ops.actions) {
890
+ if (checkOpTypes(op))
891
+ return true;
892
+ }
893
+ for (const op of ops.functions) {
894
+ if (checkOpTypes(op))
895
+ return true;
896
+ }
897
+ }
898
+ for (const op of unboundActions) {
899
+ if (checkOpTypes(op))
900
+ return true;
901
+ }
902
+ for (const op of unboundFunctions) {
903
+ if (checkOpTypes(op))
904
+ return true;
905
+ }
906
+ return false;
907
+ };
908
+ for (const setName of Array.from(operationExpandedEntitySets)) {
909
+ if (!includedEntitySets.has(setName))
910
+ continue;
911
+ if (wantedSet.has(setName))
912
+ continue;
913
+ const typeFqn = setToTypeMap.get(setName);
914
+ if (!typeFqn)
915
+ continue;
916
+ if (isEntityTypeReferencedByOperations(typeFqn))
917
+ continue;
918
+ includedEntitySets.delete(setName);
919
+ includedEntityTypes.delete(typeFqn);
920
+ }
921
+ }
922
+ // Helper to generate operation code
923
+ function generateOperationCode(op, isUnbound = false) {
924
+ const name = op.def['@_Name'];
925
+ let out = ` "${name}": {\n`;
926
+ if (isUnbound) {
927
+ out += ` type: 'unbound',\n`;
928
+ }
929
+ else {
930
+ const bindingTypeShortName = op.bindingTypeFQN ? getShortName(op.bindingTypeFQN) : '';
931
+ out += ` type: 'bound',\n`;
932
+ out += ` collection: ${op.isCollectionBound || false},\n`;
933
+ out += ` target: '${bindingTypeShortName}',\n`;
934
+ }
935
+ out += ` parameters: {\n`;
936
+ if (op.def.Parameter) {
937
+ const startIndex = op.isBound ? 1 : 0;
938
+ for (let i = startIndex; i < op.def.Parameter.length; i++) {
939
+ const param = op.def.Parameter[i];
940
+ if (!param)
941
+ continue;
942
+ const paramName = param['@_Name'];
943
+ const paramTypeCode = generateTypeCode(param['@_Type']);
944
+ out += ` "${paramName}": ${paramTypeCode},\n`;
945
+ }
946
+ }
947
+ out += ` },\n`;
948
+ if (op.def.ReturnType) {
949
+ const returnTypeCode = generateTypeCode(op.def.ReturnType['@_Type']);
950
+ out += ` returnType: ${returnTypeCode},\n`;
951
+ }
952
+ out += ` },\n`;
953
+ return out;
954
+ }
955
+ // Generate schema output
956
+ let out = `import { schema } from "o-data/schema";\n\n`;
957
+ out += `export const ${namespace.replace(/\./g, '_').toLowerCase()}_schema = schema({\n`;
958
+ out += ` namespace: "${namespace}",\n`;
959
+ if (alias) {
960
+ out += ` alias: "${alias}",\n`;
961
+ }
962
+ // Generate enumtypes
963
+ if (includedEnumTypes.size > 0) {
964
+ out += ` enumtypes: {\n`;
965
+ for (const enumFqn of Array.from(includedEnumTypes).sort()) {
966
+ const enumDef = enumTypes.get(enumFqn);
967
+ if (!enumDef)
968
+ continue;
969
+ const name = getShortName(enumFqn);
970
+ const isFlags = enumDef['@_IsFlags'] === 'true';
971
+ out += ` "${name}": {\n`;
972
+ out += ` isFlags: ${isFlags},\n`;
973
+ out += ` members: {\n`;
974
+ if (enumDef.Member) {
975
+ for (const member of enumDef.Member) {
976
+ const memberName = member['@_Name'];
977
+ const memberValue = member['@_Value'];
978
+ out += ` "${memberName}": ${memberValue},\n`;
979
+ }
980
+ }
981
+ out += ` },\n`;
982
+ out += ` },\n`;
983
+ }
984
+ out += ` },\n`;
985
+ }
986
+ // Generate complextypes
987
+ if (includedComplexTypes.size > 0) {
988
+ out += ` complextypes: {\n`;
989
+ for (const ctFqn of Array.from(includedComplexTypes).sort()) {
990
+ const ct = complexTypes.get(ctFqn);
991
+ if (!ct)
992
+ continue;
993
+ const name = getShortName(ctFqn);
994
+ out += ` "${name}": {\n`;
995
+ if (ct.Property) {
996
+ for (const prop of ct.Property) {
997
+ if (isExcluded(prop['@_Name'], 'properties'))
998
+ continue;
999
+ // Properties are directly in complextype (not nested in properties object)
1000
+ // Adjust indentation: generatePropertyCode uses 8 spaces, we need 6
1001
+ const propCode = generatePropertyCode(prop);
1002
+ out += propCode.replace(/^ /, ' ');
1003
+ }
1004
+ }
1005
+ out += ` },\n`;
1006
+ }
1007
+ out += ` },\n`;
1008
+ }
1009
+ // Generate entitytypes
1010
+ out += ` entitytypes: {\n`;
1011
+ for (const entityTypeFQN of Array.from(includedEntityTypes).sort()) {
1012
+ const entityType = entityTypes.get(entityTypeFQN);
1013
+ if (!entityType)
1014
+ continue;
1015
+ const name = getShortName(entityTypeFQN);
1016
+ out += ` "${name}": {\n`;
1017
+ // Generate baseType if present
1018
+ if (entityType['@_BaseType']) {
1019
+ const { name: baseTypeFQN } = resolveType(entityType['@_BaseType']);
1020
+ const baseTypeShortName = getShortName(baseTypeFQN);
1021
+ out += ` baseType: "${baseTypeShortName}",\n`;
1022
+ }
1023
+ // Generate properties (including navigations)
1024
+ out += ` properties: {\n`;
1025
+ // Regular properties
1026
+ if (entityType.Property) {
1027
+ for (const prop of entityType.Property) {
1028
+ if (isExcluded(prop['@_Name'], 'properties'))
1029
+ continue;
1030
+ out += generatePropertyCode(prop, entityType.Key);
1031
+ }
1032
+ }
1033
+ // Navigation properties
1034
+ if (entityType.NavigationProperty) {
1035
+ for (const nav of entityType.NavigationProperty) {
1036
+ if (isExcluded(nav['@_Name'], 'navigations'))
1037
+ continue;
1038
+ out += generateNavigationCode(nav);
1039
+ }
1040
+ }
1041
+ out += ` },\n`;
1042
+ out += ` },\n`;
1043
+ }
1044
+ out += ` },\n`;
1045
+ // Generate entitysets
1046
+ out += ` entitysets: {\n`;
1047
+ for (const setName of Array.from(includedEntitySets).sort()) {
1048
+ const typeFqn = setToTypeMap.get(setName);
1049
+ if (!typeFqn)
1050
+ continue;
1051
+ const entityTypeShortName = getShortName(typeFqn);
1052
+ out += ` "${setName}": {\n`;
1053
+ out += ` entitytype: "${entityTypeShortName}",\n`;
1054
+ out += ` },\n`;
1055
+ }
1056
+ out += ` },\n`;
1057
+ // Generate actions (bound and unbound)
1058
+ const allActions = [];
1059
+ for (const [entityTypeFQN, ops] of boundOperations) {
1060
+ allActions.push(...ops.actions);
1061
+ }
1062
+ allActions.push(...unboundActions);
1063
+ if (allActions.length > 0) {
1064
+ out += ` actions: {\n`;
1065
+ const seenActionNames = new Set();
1066
+ for (const op of allActions) {
1067
+ const name = op.def['@_Name'];
1068
+ // TODO: OData supports operation overloading where the same operation name
1069
+ // can be bound to different entity types. Currently we only keep the first
1070
+ // occurrence to avoid duplicate keys in the generated schema object.
1071
+ // In the future, we should support overloading by changing how operations
1072
+ // are keyed (e.g., using a composite key like "${name}_${bindingType}" or
1073
+ // restructuring to support multiple operations with the same name).
1074
+ if (seenActionNames.has(name)) {
1075
+ continue; // Skip duplicate - keep only first occurrence
1076
+ }
1077
+ seenActionNames.add(name);
1078
+ out += generateOperationCode(op, !op.isBound);
1079
+ }
1080
+ out += ` },\n`;
1081
+ }
1082
+ // Generate functions (bound and unbound)
1083
+ const allFunctions = [];
1084
+ for (const [entityTypeFQN, ops] of boundOperations) {
1085
+ allFunctions.push(...ops.functions);
1086
+ }
1087
+ allFunctions.push(...unboundFunctions);
1088
+ if (allFunctions.length > 0) {
1089
+ out += ` functions: {\n`;
1090
+ const seenFunctionNames = new Set();
1091
+ for (const op of allFunctions) {
1092
+ const name = op.def['@_Name'];
1093
+ // TODO: OData supports operation overloading where the same operation name
1094
+ // can be bound to different entity types. Currently we only keep the first
1095
+ // occurrence to avoid duplicate keys in the generated schema object.
1096
+ // In the future, we should support overloading by changing how operations
1097
+ // are keyed (e.g., using a composite key like "${name}_${bindingType}" or
1098
+ // restructuring to support multiple operations with the same name).
1099
+ if (seenFunctionNames.has(name)) {
1100
+ continue; // Skip duplicate - keep only first occurrence
1101
+ }
1102
+ seenFunctionNames.add(name);
1103
+ out += generateOperationCode(op, !op.isBound);
1104
+ }
1105
+ out += ` },\n`;
1106
+ }
1107
+ // Generate actionImports
1108
+ if (actionImports.size > 0) {
1109
+ out += ` actionImports: {\n`;
1110
+ for (const [importName, actionFQN] of Array.from(actionImports.entries()).sort()) {
1111
+ const actionShortName = getShortName(actionFQN);
1112
+ // Check if action is excluded
1113
+ if (isExcluded(actionShortName, 'actions')) {
1114
+ continue; // Skip excluded actions
1115
+ }
1116
+ // Check if this action is actually included (bound or unbound)
1117
+ const isIncluded = allActions.some(op => op.def['@_Name'] === actionShortName);
1118
+ if (!isIncluded) {
1119
+ continue; // Skip if action not included
1120
+ }
1121
+ out += ` "${importName}": {\n`;
1122
+ out += ` action: "${actionShortName}",\n`;
1123
+ out += ` },\n`;
1124
+ }
1125
+ out += ` },\n`;
1126
+ }
1127
+ // Generate functionImports
1128
+ if (functionImports.size > 0) {
1129
+ out += ` functionImports: {\n`;
1130
+ for (const [importName, functionFQN] of Array.from(functionImports.entries()).sort()) {
1131
+ const functionShortName = getShortName(functionFQN);
1132
+ // Check if function is excluded
1133
+ if (isExcluded(functionShortName, 'functions')) {
1134
+ continue; // Skip excluded functions
1135
+ }
1136
+ // Check if this function is actually included (bound or unbound)
1137
+ const isIncluded = allFunctions.some(op => op.def['@_Name'] === functionShortName);
1138
+ if (!isIncluded) {
1139
+ continue; // Skip if function not included
1140
+ }
1141
+ out += ` "${importName}": {\n`;
1142
+ out += ` function: "${functionShortName}",\n`;
1143
+ out += ` },\n`;
1144
+ }
1145
+ out += ` },\n`;
1146
+ }
1147
+ out += `});\n`;
1148
+ const dir = dirname(OUTPUT_FILE);
1149
+ if (!fs.existsSync(dir)) {
1150
+ fs.mkdirSync(dir, { recursive: true });
1151
+ }
1152
+ fs.writeFileSync(OUTPUT_FILE, out);
1153
+ console.log(`Filtered Schema generated at ${OUTPUT_FILE}`);
1154
+ console.log(`Included EntitySets: ${Array.from(includedEntitySets).sort().join(', ')}`);
1155
+ console.log(`Included EntityTypes: ${Array.from(includedEntityTypes).map(getShortName).sort().join(', ')}`);
1156
+ console.log(`Included ComplexTypes: ${Array.from(includedComplexTypes).map(getShortName).sort().join(', ')}`);
1157
+ }