@odata-effect/odata-effect-generator 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/cjs/Cli.js +19 -3
  2. package/dist/cjs/Cli.js.map +1 -1
  3. package/dist/cjs/digester/Digester.js +34 -30
  4. package/dist/cjs/digester/Digester.js.map +1 -1
  5. package/dist/cjs/generator/Generator.js.map +1 -1
  6. package/dist/cjs/generator/ModelsGenerator.js +26 -2
  7. package/dist/cjs/generator/ModelsGenerator.js.map +1 -1
  8. package/dist/cjs/generator/NamingHelper.js +90 -1
  9. package/dist/cjs/generator/NamingHelper.js.map +1 -1
  10. package/dist/cjs/generator/OperationsGenerator.js +44 -16
  11. package/dist/cjs/generator/OperationsGenerator.js.map +1 -1
  12. package/dist/cjs/index.js +7 -3
  13. package/dist/cjs/model/DataModel.js.map +1 -1
  14. package/dist/cjs/model/GeneratorConfig.js +70 -0
  15. package/dist/cjs/model/GeneratorConfig.js.map +1 -0
  16. package/dist/dts/Cli.d.ts.map +1 -1
  17. package/dist/dts/digester/Digester.d.ts +2 -1
  18. package/dist/dts/digester/Digester.d.ts.map +1 -1
  19. package/dist/dts/generator/Generator.d.ts +7 -4
  20. package/dist/dts/generator/Generator.d.ts.map +1 -1
  21. package/dist/dts/generator/NamingHelper.d.ts +49 -0
  22. package/dist/dts/generator/NamingHelper.d.ts.map +1 -1
  23. package/dist/dts/generator/OperationsGenerator.d.ts +1 -0
  24. package/dist/dts/generator/OperationsGenerator.d.ts.map +1 -1
  25. package/dist/dts/index.d.ts +41 -12
  26. package/dist/dts/index.d.ts.map +1 -1
  27. package/dist/dts/model/DataModel.d.ts +1 -0
  28. package/dist/dts/model/DataModel.d.ts.map +1 -1
  29. package/dist/dts/model/GeneratorConfig.d.ts +114 -0
  30. package/dist/dts/model/GeneratorConfig.d.ts.map +1 -0
  31. package/dist/esm/Cli.js +19 -3
  32. package/dist/esm/Cli.js.map +1 -1
  33. package/dist/esm/digester/Digester.js +35 -31
  34. package/dist/esm/digester/Digester.js.map +1 -1
  35. package/dist/esm/generator/Generator.js.map +1 -1
  36. package/dist/esm/generator/ModelsGenerator.js +26 -2
  37. package/dist/esm/generator/ModelsGenerator.js.map +1 -1
  38. package/dist/esm/generator/NamingHelper.js +84 -0
  39. package/dist/esm/generator/NamingHelper.js.map +1 -1
  40. package/dist/esm/generator/OperationsGenerator.js +45 -17
  41. package/dist/esm/generator/OperationsGenerator.js.map +1 -1
  42. package/dist/esm/index.js +41 -12
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/model/DataModel.js.map +1 -1
  45. package/dist/esm/model/GeneratorConfig.js +60 -0
  46. package/dist/esm/model/GeneratorConfig.js.map +1 -0
  47. package/model/GeneratorConfig/package.json +6 -0
  48. package/package.json +9 -1
  49. package/src/Cli.ts +26 -3
  50. package/src/digester/Digester.ts +55 -29
  51. package/src/generator/Generator.ts +7 -4
  52. package/src/generator/ModelsGenerator.ts +31 -2
  53. package/src/generator/NamingHelper.ts +115 -0
  54. package/src/generator/OperationsGenerator.ts +51 -18
  55. package/src/index.ts +43 -12
  56. package/src/model/DataModel.ts +1 -0
  57. package/src/model/GeneratorConfig.ts +180 -0
@@ -6,7 +6,14 @@
6
6
  */
7
7
  import * as Effect from "effect/Effect"
8
8
  import * as Schema from "effect/Schema"
9
- import { getClassName, getPropertyName } from "../generator/NamingHelper.js"
9
+ import {
10
+ getClassName,
11
+ getClassNameWithOverrides,
12
+ getOperationNameWithOverrides,
13
+ getOperationParameterNameWithOverrides,
14
+ getPropertyNameWithOverrides
15
+ } from "../generator/NamingHelper.js"
16
+ import type { NamingOverrides } from "../model/GeneratorConfig.js"
10
17
  import {
11
18
  type ComplexTypeModel,
12
19
  createDataModel,
@@ -69,6 +76,7 @@ interface DigestContext {
69
76
  readonly enumTypes: Set<string>
70
77
  readonly complexTypes: Set<string>
71
78
  readonly entityTypes: Set<string>
79
+ readonly overrides?: NamingOverrides | undefined
72
80
  }
73
81
 
74
82
  /**
@@ -78,7 +86,8 @@ interface DigestContext {
78
86
  * @category digestion
79
87
  */
80
88
  export const digestMetadata = (
81
- edmx: ODataEdmxModel
89
+ edmx: ODataEdmxModel,
90
+ overrides?: NamingOverrides
82
91
  ): Effect.Effect<DataModel, DigestError> =>
83
92
  Effect.try({
84
93
  try: () => {
@@ -107,7 +116,7 @@ export const digestMetadata = (
107
116
  const dataModel = createDataModel(version, namespace, serviceName)
108
117
 
109
118
  // Build context for type resolution
110
- const context = buildContext(version, namespace, schemas)
119
+ const context = buildContext(version, namespace, schemas, overrides)
111
120
 
112
121
  // First pass: collect all type names
113
122
  for (const schema of schemas) {
@@ -139,7 +148,8 @@ export const digestMetadata = (
139
148
  const buildContext = (
140
149
  version: ODataVersion,
141
150
  namespace: string,
142
- schemas: ReadonlyArray<EdmxSchema>
151
+ schemas: ReadonlyArray<EdmxSchema>,
152
+ overrides?: NamingOverrides
143
153
  ): DigestContext => {
144
154
  const associations = new Map<string, Association>()
145
155
 
@@ -159,7 +169,8 @@ const buildContext = (
159
169
  associations,
160
170
  enumTypes: new Set(),
161
171
  complexTypes: new Set(),
162
- entityTypes: new Set()
172
+ entityTypes: new Set(),
173
+ overrides
163
174
  }
164
175
  }
165
176
 
@@ -265,14 +276,18 @@ const digestComplexType = (
265
276
  namespace: string,
266
277
  context: DigestContext
267
278
  ): ComplexTypeModel => {
268
- const name = complexType.$.Name
269
- const properties = (complexType.Property ?? []).map((p) => digestProperty(p, [], context))
270
- const navigationProperties = (complexType.NavigationProperty ?? []).map((np) => digestNavigationProperty(np, context))
279
+ const odataTypeName = complexType.$.Name
280
+ const properties = (complexType.Property ?? []).map((p) =>
281
+ digestProperty(p, [], odataTypeName, "complex", context)
282
+ )
283
+ const navigationProperties = (complexType.NavigationProperty ?? []).map((np) =>
284
+ digestNavigationProperty(np, odataTypeName, "complex", context)
285
+ )
271
286
 
272
287
  const result: ComplexTypeModel = {
273
- fqName: `${namespace}.${name}`,
274
- odataName: name,
275
- name: getClassName(name),
288
+ fqName: `${namespace}.${odataTypeName}`,
289
+ odataName: odataTypeName,
290
+ name: getClassNameWithOverrides(odataTypeName, "complex", context.overrides),
276
291
  properties,
277
292
  navigationProperties,
278
293
  isAbstract: complexType.$.Abstract === "true",
@@ -294,7 +309,7 @@ const digestEntityType = (
294
309
  namespace: string,
295
310
  context: DigestContext
296
311
  ): EntityTypeModel => {
297
- const name = entityType.$.Name
312
+ const odataTypeName = entityType.$.Name
298
313
  const keyNames = new Set<string>()
299
314
 
300
315
  // Collect key property names
@@ -304,15 +319,19 @@ const digestEntityType = (
304
319
  }
305
320
  }
306
321
 
307
- const properties = (entityType.Property ?? []).map((p) => digestProperty(p, Array.from(keyNames), context))
322
+ const properties = (entityType.Property ?? []).map((p) =>
323
+ digestProperty(p, Array.from(keyNames), odataTypeName, "entity", context)
324
+ )
308
325
 
309
326
  const keys = properties.filter((p) => p.isKey)
310
- const navigationProperties = (entityType.NavigationProperty ?? []).map((np) => digestNavigationProperty(np, context))
327
+ const navigationProperties = (entityType.NavigationProperty ?? []).map((np) =>
328
+ digestNavigationProperty(np, odataTypeName, "entity", context)
329
+ )
311
330
 
312
331
  const result: EntityTypeModel = {
313
- fqName: `${namespace}.${name}`,
314
- odataName: name,
315
- name: getClassName(name),
332
+ fqName: `${namespace}.${odataTypeName}`,
333
+ odataName: odataTypeName,
334
+ name: getClassNameWithOverrides(odataTypeName, "entity", context.overrides),
316
335
  keys,
317
336
  properties,
318
337
  navigationProperties,
@@ -333,6 +352,8 @@ const digestEntityType = (
333
352
  const digestProperty = (
334
353
  property: Property,
335
354
  keyNames: ReadonlyArray<string>,
355
+ ownerTypeName: string,
356
+ ownerTypeKind: "entity" | "complex",
336
357
  context: DigestContext
337
358
  ): PropertyModel => {
338
359
  const odataName = property.$.Name
@@ -345,7 +366,7 @@ const digestProperty = (
345
366
 
346
367
  const result: PropertyModel = {
347
368
  odataName,
348
- name: getPropertyName(odataName),
369
+ name: getPropertyNameWithOverrides(odataName, ownerTypeName, ownerTypeKind, context.overrides),
349
370
  odataType,
350
371
  typeMapping,
351
372
  isCollection,
@@ -380,6 +401,8 @@ const isV4NavigationProperty = (
380
401
  */
381
402
  const digestNavigationProperty = (
382
403
  navProp: NavigationProperty,
404
+ ownerTypeName: string,
405
+ ownerTypeKind: "entity" | "complex",
383
406
  context: DigestContext
384
407
  ): NavigationPropertyModel => {
385
408
  const odataName = navProp.$.Name
@@ -391,7 +414,7 @@ const digestNavigationProperty = (
391
414
 
392
415
  const result: NavigationPropertyModel = {
393
416
  odataName,
394
- name: getPropertyName(odataName),
417
+ name: getPropertyNameWithOverrides(odataName, ownerTypeName, ownerTypeKind, context.overrides),
395
418
  targetType: targetTypeName,
396
419
  isCollection,
397
420
  isNullable: navProp.$.Nullable !== "false"
@@ -423,7 +446,7 @@ const digestNavigationProperty = (
423
446
 
424
447
  return {
425
448
  odataName,
426
- name: getPropertyName(odataName),
449
+ name: getPropertyNameWithOverrides(odataName, ownerTypeName, ownerTypeKind, context.overrides),
427
450
  targetType,
428
451
  isCollection,
429
452
  isNullable: true
@@ -467,10 +490,12 @@ const digestEntityContainer = (
467
490
  for (const funcImport of container.FunctionImport) {
468
491
  // Only process V2 function imports (no Function reference)
469
492
  if (!funcImport.$.Function) {
493
+ const odataOpName = funcImport.$.Name
470
494
  const params: Array<OperationParameterModel> = (funcImport.Parameter ?? []).map((p) => {
471
495
  const { baseType, isCollection } = parseODataType(p.$.Type)
472
496
  return {
473
- name: getPropertyName(p.$.Name),
497
+ name: getOperationParameterNameWithOverrides(p.$.Name, odataOpName, context.overrides),
498
+ odataName: p.$.Name,
474
499
  odataType: p.$.Type,
475
500
  typeMapping: resolveTypeMapping(baseType, context),
476
501
  isNullable: p.$.Nullable !== "false",
@@ -479,9 +504,9 @@ const digestEntityContainer = (
479
504
  })
480
505
 
481
506
  let model: OperationModel = {
482
- fqName: `${context.namespace}.${funcImport.$.Name}`,
483
- odataName: funcImport.$.Name,
484
- name: getPropertyName(funcImport.$.Name),
507
+ fqName: `${context.namespace}.${odataOpName}`,
508
+ odataName: odataOpName,
509
+ name: getOperationNameWithOverrides(odataOpName, context.overrides),
485
510
  type: "Function",
486
511
  isBound: false,
487
512
  parameters: params
@@ -519,13 +544,14 @@ const digestOperation = (
519
544
  type: "Function" | "Action",
520
545
  context: DigestContext
521
546
  ): OperationModel => {
522
- const name = operation.$.Name
547
+ const odataName = operation.$.Name
523
548
  const isBound = operation.$.IsBound === "true"
524
549
 
525
550
  const allParams = (operation.Parameter ?? []).map((p) => {
526
551
  const { baseType, isCollection } = parseODataType(p.$.Type)
527
552
  return {
528
- name: getPropertyName(p.$.Name),
553
+ name: getOperationParameterNameWithOverrides(p.$.Name, odataName, context.overrides),
554
+ odataName: p.$.Name,
529
555
  odataType: p.$.Type,
530
556
  typeMapping: resolveTypeMapping(baseType, context),
531
557
  isNullable: p.$.Nullable !== "false",
@@ -538,9 +564,9 @@ const digestOperation = (
538
564
  const parameters = isBound ? allParams.slice(1) : allParams
539
565
 
540
566
  let model: OperationModel = {
541
- fqName: `${namespace}.${name}`,
542
- odataName: name,
543
- name: getPropertyName(name),
567
+ fqName: `${namespace}.${odataName}`,
568
+ odataName,
569
+ name: getOperationNameWithOverrides(odataName, context.overrides),
544
570
  type,
545
571
  isBound,
546
572
  parameters
@@ -8,6 +8,7 @@ import * as Path from "@effect/platform/Path"
8
8
  import * as Effect from "effect/Effect"
9
9
  import * as Schema from "effect/Schema"
10
10
  import type { DataModel } from "../model/DataModel.js"
11
+ import type { NamingOverrides } from "../model/GeneratorConfig.js"
11
12
  import { generateIndex } from "./IndexGenerator.js"
12
13
  import { generateModels } from "./ModelsGenerator.js"
13
14
  import { generateNavigations } from "./NavigationGenerator.js"
@@ -32,11 +33,13 @@ import { generateServiceFns } from "./ServiceFnGenerator.js"
32
33
  */
33
34
  export interface GeneratorConfig {
34
35
  readonly outputDir: string
35
- readonly packageName?: string
36
- readonly serviceName?: string
37
- readonly force?: boolean
36
+ readonly packageName?: string | undefined
37
+ readonly serviceName?: string | undefined
38
+ readonly force?: boolean | undefined
38
39
  /** Generate only source files directly in outputDir (no package.json, tsconfig, src/ subdirectory) */
39
- readonly filesOnly?: boolean
40
+ readonly filesOnly?: boolean | undefined
41
+ /** Optional naming overrides for properties and types */
42
+ readonly overrides?: NamingOverrides | undefined
40
43
  }
41
44
 
42
45
  /**
@@ -291,7 +291,8 @@ const generateSchemaFields = (
291
291
  const prop = properties[i]
292
292
  const schemaType = getPropertySchemaType(prop, prop.isNullable && !prop.isKey)
293
293
  const isLast = i === properties.length - 1
294
- fields.push(`${prop.name}: ${schemaType}${isLast ? "" : ","}`)
294
+ const fieldDef = getPropertyFieldDefinition(prop, schemaType)
295
+ fields.push(`${fieldDef}${isLast ? "" : ","}`)
295
296
  }
296
297
 
297
298
  // Navigation properties are intentionally excluded from Schema.Class
@@ -312,12 +313,40 @@ const generateEditableSchemaFields = (
312
313
  const prop = properties[i]
313
314
  const schemaType = getPropertySchemaType(prop, prop.isNullable)
314
315
  const isLast = i === properties.length - 1
315
- fields.push(`${prop.name}: ${schemaType}${isLast ? "" : ","}`)
316
+ const fieldDef = getPropertyFieldDefinition(prop, schemaType)
317
+ fields.push(`${fieldDef}${isLast ? "" : ","}`)
316
318
  }
317
319
 
318
320
  return fields
319
321
  }
320
322
 
323
+ /**
324
+ * Get a complete property field definition including fromKey mapping if needed.
325
+ *
326
+ * When the OData property name differs from the TypeScript property name,
327
+ * we use Schema.propertySignature with Schema.fromKey to map between them.
328
+ * This is essential for OData V2 responses which use PascalCase property names.
329
+ *
330
+ * @example
331
+ * // When odataName == name:
332
+ * name: Schema.String
333
+ *
334
+ * // When odataName ("ID") != name ("id"):
335
+ * id: Schema.propertySignature(Schema.Number).pipe(Schema.fromKey("ID"))
336
+ */
337
+ const getPropertyFieldDefinition = (
338
+ prop: PropertyModel,
339
+ schemaType: string
340
+ ): string => {
341
+ // If OData name matches TypeScript name, use simple format
342
+ if (prop.odataName === prop.name) {
343
+ return `${prop.name}: ${schemaType}`
344
+ }
345
+
346
+ // Use propertySignature with fromKey to map between encoded (OData) and decoded (TypeScript) names
347
+ return `${prop.name}: Schema.propertySignature(${schemaType}).pipe(Schema.fromKey("${prop.odataName}"))`
348
+ }
349
+
321
350
  /**
322
351
  * Get the Schema type for a property.
323
352
  */
@@ -3,6 +3,13 @@
3
3
  *
4
4
  * @since 1.0.0
5
5
  */
6
+ import {
7
+ getOperationOverride,
8
+ getOperationParameterOverride,
9
+ getPropertyOverride,
10
+ getTypeOverride,
11
+ type NamingOverrides
12
+ } from "../model/GeneratorConfig.js"
6
13
 
7
14
  /**
8
15
  * Convert a string to PascalCase.
@@ -164,3 +171,111 @@ export const getEditableTypeName = (typeName: string): string => {
164
171
  export const getIdTypeName = (typeName: string): string => {
165
172
  return `${toPascalCase(typeName)}Id`
166
173
  }
174
+
175
+ // ============================================================================
176
+ // Override-aware naming functions
177
+ // ============================================================================
178
+
179
+ /**
180
+ * Get the TypeScript property name for an OData property, with override support.
181
+ *
182
+ * @param odataName - The original OData property name
183
+ * @param typeName - The OData type name this property belongs to
184
+ * @param typeKind - Whether the type is an entity or complex type
185
+ * @param overrides - Optional naming overrides configuration
186
+ * @returns The TypeScript property name
187
+ *
188
+ * @since 1.0.0
189
+ * @category naming
190
+ */
191
+ export const getPropertyNameWithOverrides = (
192
+ odataName: string,
193
+ typeName: string,
194
+ typeKind: "entity" | "complex",
195
+ overrides?: NamingOverrides
196
+ ): string => {
197
+ // Check for override first
198
+ const override = getPropertyOverride(overrides, typeName, typeKind, odataName)
199
+ if (override) {
200
+ return override
201
+ }
202
+
203
+ // Fall back to default camelCase conversion
204
+ return getPropertyName(odataName)
205
+ }
206
+
207
+ /**
208
+ * Get the TypeScript class name for an OData type, with override support.
209
+ *
210
+ * @param odataName - The original OData type name
211
+ * @param typeKind - Whether the type is an entity or complex type
212
+ * @param overrides - Optional naming overrides configuration
213
+ * @returns The TypeScript class name
214
+ *
215
+ * @since 1.0.0
216
+ * @category naming
217
+ */
218
+ export const getClassNameWithOverrides = (
219
+ odataName: string,
220
+ typeKind: "entity" | "complex",
221
+ overrides?: NamingOverrides
222
+ ): string => {
223
+ // Check for override first
224
+ const override = getTypeOverride(overrides, odataName, typeKind)
225
+ if (override) {
226
+ return override
227
+ }
228
+
229
+ // Fall back to default PascalCase conversion
230
+ return getClassName(odataName)
231
+ }
232
+
233
+ /**
234
+ * Get the TypeScript function name for an OData operation, with override support.
235
+ *
236
+ * @param odataName - The original OData operation name
237
+ * @param overrides - Optional naming overrides configuration
238
+ * @returns The TypeScript function name (camelCase)
239
+ *
240
+ * @since 1.0.0
241
+ * @category naming
242
+ */
243
+ export const getOperationNameWithOverrides = (
244
+ odataName: string,
245
+ overrides?: NamingOverrides
246
+ ): string => {
247
+ // Check for override first
248
+ const override = getOperationOverride(overrides, odataName)
249
+ if (override) {
250
+ return override
251
+ }
252
+
253
+ // Fall back to default camelCase conversion
254
+ return getPropertyName(odataName)
255
+ }
256
+
257
+ /**
258
+ * Get the TypeScript parameter name for an OData operation parameter, with override support.
259
+ *
260
+ * @param odataParamName - The original OData parameter name
261
+ * @param odataOperationName - The OData operation this parameter belongs to
262
+ * @param overrides - Optional naming overrides configuration
263
+ * @returns The TypeScript parameter name (camelCase)
264
+ *
265
+ * @since 1.0.0
266
+ * @category naming
267
+ */
268
+ export const getOperationParameterNameWithOverrides = (
269
+ odataParamName: string,
270
+ odataOperationName: string,
271
+ overrides?: NamingOverrides
272
+ ): string => {
273
+ // Check for override first
274
+ const override = getOperationParameterOverride(overrides, odataOperationName, odataParamName)
275
+ if (override) {
276
+ return override
277
+ }
278
+
279
+ // Fall back to default camelCase conversion
280
+ return getPropertyName(odataParamName)
281
+ }
@@ -6,9 +6,9 @@
6
6
  *
7
7
  * @since 1.0.0
8
8
  */
9
- import type { DataModel, OperationModel } from "../model/DataModel.js"
9
+ import type { DataModel, OperationModel, OperationParameterModel } from "../model/DataModel.js"
10
10
  import type { ODataVersion } from "../parser/EdmxSchema.js"
11
- import { toCamelCase, toPascalCase } from "./NamingHelper.js"
11
+ import { toPascalCase } from "./NamingHelper.js"
12
12
 
13
13
  /**
14
14
  * Version-specific imports and identifiers.
@@ -22,25 +22,26 @@ interface VersionConfig {
22
22
  const V2_CONFIG: VersionConfig = {
23
23
  clientConfigTag: "ODataClientConfig",
24
24
  clientConfigImport: "ODataClientConfig",
25
- clientModule: "ODataClient"
25
+ clientModule: "OData"
26
26
  }
27
27
 
28
28
  const V4_CONFIG: VersionConfig = {
29
29
  clientConfigTag: "ODataV4ClientConfig",
30
30
  clientConfigImport: "ODataV4ClientConfig",
31
- clientModule: "ODataV4Client"
31
+ clientModule: "ODataV4"
32
32
  }
33
33
 
34
34
  const getVersionConfig = (version: ODataVersion): VersionConfig => version === "V4" ? V4_CONFIG : V2_CONFIG
35
35
 
36
36
  /**
37
37
  * Get the function name for an operation.
38
+ * Note: operation.name is already properly cased by the Digester.
38
39
  *
39
40
  * @since 1.0.0
40
41
  * @category naming
41
42
  */
42
43
  export const getOperationFunctionName = (operation: OperationModel): string => {
43
- return toCamelCase(operation.name)
44
+ return operation.name
44
45
  }
45
46
 
46
47
  /**
@@ -274,7 +275,7 @@ const generateParameterType = (
274
275
  ): string | null => {
275
276
  if (operation.parameters.length === 0) return null
276
277
 
277
- const typeName = `${toPascalCase(operation.name)}Params`
278
+ const typeName = `${toPascalCase(operation.odataName)}Params`
278
279
  return typeName
279
280
  }
280
281
 
@@ -287,7 +288,7 @@ const generateParameterInterface = (
287
288
  ): void => {
288
289
  if (operation.parameters.length === 0) return
289
290
 
290
- const typeName = `${toPascalCase(operation.name)}Params`
291
+ const typeName = `${toPascalCase(operation.odataName)}Params`
291
292
 
292
293
  lines.push(`/**`)
293
294
  lines.push(` * Parameters for ${operation.odataName} operation.`)
@@ -309,6 +310,26 @@ const generateParameterInterface = (
309
310
  lines.push(``)
310
311
  }
311
312
 
313
+ /**
314
+ * Build the parameters object mapping TypeScript names to OData names.
315
+ */
316
+ const generateParametersObject = (
317
+ lines: Array<string>,
318
+ params: ReadonlyArray<OperationParameterModel>,
319
+ indent: string
320
+ ): void => {
321
+ lines.push(`${indent}const parameters: ODataOps.OperationParameters = {`)
322
+ for (const param of params) {
323
+ // Map TypeScript param name to OData param name
324
+ if (param.name === param.odataName) {
325
+ lines.push(`${indent} ${param.name}: params.${param.name},`)
326
+ } else {
327
+ lines.push(`${indent} "${param.odataName}": params.${param.name},`)
328
+ }
329
+ }
330
+ lines.push(`${indent}}`)
331
+ }
332
+
312
333
  /**
313
334
  * Generate V2 FunctionImport function.
314
335
  */
@@ -352,11 +373,7 @@ const generateV2FunctionImport = (
352
373
 
353
374
  // Build parameters object for the function import URL
354
375
  if (paramsType) {
355
- lines.push(` const parameters: ODataOps.OperationParameters = {`)
356
- for (const param of operation.parameters) {
357
- lines.push(` ${param.name}: params.${param.name},`)
358
- }
359
- lines.push(` }`)
376
+ generateParametersObject(lines, operation.parameters, " ")
360
377
  lines.push(``)
361
378
  }
362
379
 
@@ -438,11 +455,7 @@ const generateV4Operation = (
438
455
  if (!isAction) {
439
456
  // V4 Functions use GET with parameters in URL
440
457
  if (paramsType) {
441
- lines.push(` const parameters: ODataOps.OperationParameters = {`)
442
- for (const param of operation.parameters) {
443
- lines.push(` ${param.name}: params.${param.name},`)
444
- }
445
- lines.push(` }`)
458
+ generateParametersObject(lines, operation.parameters, " ")
446
459
  lines.push(` const url = ODataOps.buildV4FunctionUrl("${operation.odataName}", parameters)`)
447
460
  } else {
448
461
  lines.push(` const url = ODataOps.buildV4FunctionUrl("${operation.odataName}")`)
@@ -477,7 +490,27 @@ const generateV4Operation = (
477
490
  lines.push(` const url = "${operation.odataName}"`)
478
491
  lines.push(``)
479
492
 
480
- const bodyArg = paramsType ? "params" : "undefined"
493
+ // Build body with OData parameter names
494
+ let bodyArg = "undefined"
495
+ if (paramsType) {
496
+ // Check if any parameter names differ
497
+ const hasDifferentNames = operation.parameters.some((p) => p.name !== p.odataName)
498
+ if (hasDifferentNames) {
499
+ lines.push(` const body = {`)
500
+ for (const param of operation.parameters) {
501
+ if (param.name === param.odataName) {
502
+ lines.push(` ${param.name}: params.${param.name},`)
503
+ } else {
504
+ lines.push(` "${param.odataName}": params.${param.name},`)
505
+ }
506
+ }
507
+ lines.push(` }`)
508
+ lines.push(``)
509
+ bodyArg = "body"
510
+ } else {
511
+ bodyArg = "params"
512
+ }
513
+ }
481
514
 
482
515
  if (!operation.returnType) {
483
516
  lines.push(` return yield* ODataOps.executeV4ActionVoid(client, config, url, ${bodyArg})`)
package/src/index.ts CHANGED
@@ -55,6 +55,41 @@ export * as ModelsGenerator from "./generator/ModelsGenerator.js"
55
55
  */
56
56
  export * as NamingHelper from "./generator/NamingHelper.js"
57
57
 
58
+ /**
59
+ * Generator for type-safe, tree-shakable navigation path builders.
60
+ *
61
+ * Generates branded path types and navigation functions that can be composed
62
+ * with pipe() for type-safe OData path construction.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * import { pipe } from "effect"
67
+ * import { People, byKey, trips, planItems, asFlight } from "./PathBuilders"
68
+ *
69
+ * const path = pipe(
70
+ * People,
71
+ * byKey("russellwhyte"),
72
+ * trips,
73
+ * byKey(0),
74
+ * planItems,
75
+ * asFlight
76
+ * )
77
+ * ```
78
+ *
79
+ * @since 1.0.0
80
+ */
81
+ export * as NavigationGenerator from "./generator/NavigationGenerator.js"
82
+
83
+ /**
84
+ * Generator for OData operations (FunctionImports, Functions, Actions).
85
+ *
86
+ * OData V2: FunctionImports
87
+ * OData V4: Functions (GET, no side effects) and Actions (POST, with side effects)
88
+ *
89
+ * @since 1.0.0
90
+ */
91
+ export * as OperationsGenerator from "./generator/OperationsGenerator.js"
92
+
58
93
  /**
59
94
  * Generator for package configuration files.
60
95
  *
@@ -70,33 +105,29 @@ export * as PackageGenerator from "./generator/PackageGenerator.js"
70
105
  export * as QueryModelsGenerator from "./generator/QueryModelsGenerator.js"
71
106
 
72
107
  /**
73
- * Generator for tree-shakable service functions.
108
+ * Generator for entity services using the crud factory.
74
109
  *
75
- * This module generates standalone functions that can be tree-shaken.
76
- * Each entity service is a module of standalone functions that use
77
- * the tree-shakable OData functions directly.
110
+ * This module generates a single Services.ts file that creates CRUD services
111
+ * for all entity sets using the crud factory from @odata-effect/odata-effect.
78
112
  *
79
113
  * @since 1.0.0
80
114
  */
81
115
  export * as ServiceFnGenerator from "./generator/ServiceFnGenerator.js"
82
116
 
83
117
  /**
84
- * Generator for tree-shakable navigation path builders.
85
- *
86
- * This module generates pipe-based navigation functions and the toPromise
87
- * helper for Promise-based environments.
118
+ * Intermediate representation for OData metadata.
119
+ * This is the central data model that digesters create and generators consume.
88
120
  *
89
121
  * @since 1.0.0
90
122
  */
91
- export * as NavigationGenerator from "./generator/NavigationGenerator.js"
123
+ export * as DataModel from "./model/DataModel.js"
92
124
 
93
125
  /**
94
- * Intermediate representation for OData metadata.
95
- * This is the central data model that digesters create and generators consume.
126
+ * Configuration for code generation including name overrides.
96
127
  *
97
128
  * @since 1.0.0
98
129
  */
99
- export * as DataModel from "./model/DataModel.js"
130
+ export * as GeneratorConfig from "./model/GeneratorConfig.js"
100
131
 
101
132
  /**
102
133
  * Common EDMX schema interfaces for OData metadata parsing.
@@ -197,6 +197,7 @@ export interface SingletonModel {
197
197
  * @category model
198
198
  */
199
199
  export interface OperationParameterModel {
200
+ readonly odataName: string
200
201
  readonly name: string
201
202
  readonly odataType: string
202
203
  readonly typeMapping: TypeMapping