@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.
- package/README.md +416 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +13 -0
- package/dist/filter.d.ts +40 -0
- package/dist/filter.js +278 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +419 -0
- package/dist/operations.d.ts +75 -0
- package/dist/operations.js +3 -0
- package/dist/parser/config.d.ts +152 -0
- package/dist/parser/config.js +3 -0
- package/dist/parser/index.d.ts +1 -0
- package/dist/parser/index.js +1157 -0
- package/dist/query.d.ts +43 -0
- package/dist/query.js +3 -0
- package/dist/response.d.ts +132 -0
- package/dist/response.js +3 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +113 -0
- package/dist/schema.d.ts +128 -0
- package/dist/schema.js +11 -0
- package/dist/serialization.d.ts +42 -0
- package/dist/serialization.js +533 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +3 -0
- package/package.json +34 -0
|
@@ -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
|
+
}
|