@odata-effect/odata-effect-generator 0.4.1 → 0.6.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 (67) hide show
  1. package/dist/cjs/digester/TypeMapper.js +98 -22
  2. package/dist/cjs/digester/TypeMapper.js.map +1 -1
  3. package/dist/cjs/generator/Generator.js +10 -5
  4. package/dist/cjs/generator/Generator.js.map +1 -1
  5. package/dist/cjs/generator/IndexGenerator.js +7 -6
  6. package/dist/cjs/generator/IndexGenerator.js.map +1 -1
  7. package/dist/cjs/generator/ModelsGenerator.js +44 -0
  8. package/dist/cjs/generator/ModelsGenerator.js.map +1 -1
  9. package/dist/cjs/generator/NamingHelper.js +23 -1
  10. package/dist/cjs/generator/NamingHelper.js.map +1 -1
  11. package/dist/cjs/generator/NavigationGenerator.js +5 -4
  12. package/dist/cjs/generator/NavigationGenerator.js.map +1 -1
  13. package/dist/cjs/generator/OperationsGenerator.js +5 -4
  14. package/dist/cjs/generator/OperationsGenerator.js.map +1 -1
  15. package/dist/cjs/generator/QueryModelsGenerator.js +3 -2
  16. package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -1
  17. package/dist/cjs/generator/ServiceFnGenerator.js +5 -4
  18. package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -1
  19. package/dist/cjs/model/GeneratorConfig.js.map +1 -1
  20. package/dist/dts/digester/TypeMapper.d.ts.map +1 -1
  21. package/dist/dts/generator/Generator.d.ts +6 -0
  22. package/dist/dts/generator/Generator.d.ts.map +1 -1
  23. package/dist/dts/generator/IndexGenerator.d.ts +14 -1
  24. package/dist/dts/generator/IndexGenerator.d.ts.map +1 -1
  25. package/dist/dts/generator/ModelsGenerator.d.ts.map +1 -1
  26. package/dist/dts/generator/NamingHelper.d.ts +15 -0
  27. package/dist/dts/generator/NamingHelper.d.ts.map +1 -1
  28. package/dist/dts/generator/NavigationGenerator.d.ts +14 -1
  29. package/dist/dts/generator/NavigationGenerator.d.ts.map +1 -1
  30. package/dist/dts/generator/OperationsGenerator.d.ts +14 -1
  31. package/dist/dts/generator/OperationsGenerator.d.ts.map +1 -1
  32. package/dist/dts/generator/QueryModelsGenerator.d.ts +14 -1
  33. package/dist/dts/generator/QueryModelsGenerator.d.ts.map +1 -1
  34. package/dist/dts/generator/ServiceFnGenerator.d.ts +14 -1
  35. package/dist/dts/generator/ServiceFnGenerator.d.ts.map +1 -1
  36. package/dist/dts/model/GeneratorConfig.d.ts +20 -0
  37. package/dist/dts/model/GeneratorConfig.d.ts.map +1 -1
  38. package/dist/esm/digester/TypeMapper.js +98 -22
  39. package/dist/esm/digester/TypeMapper.js.map +1 -1
  40. package/dist/esm/generator/Generator.js +10 -5
  41. package/dist/esm/generator/Generator.js.map +1 -1
  42. package/dist/esm/generator/IndexGenerator.js +8 -7
  43. package/dist/esm/generator/IndexGenerator.js.map +1 -1
  44. package/dist/esm/generator/ModelsGenerator.js +44 -0
  45. package/dist/esm/generator/ModelsGenerator.js.map +1 -1
  46. package/dist/esm/generator/NamingHelper.js +21 -0
  47. package/dist/esm/generator/NamingHelper.js.map +1 -1
  48. package/dist/esm/generator/NavigationGenerator.js +6 -5
  49. package/dist/esm/generator/NavigationGenerator.js.map +1 -1
  50. package/dist/esm/generator/OperationsGenerator.js +6 -5
  51. package/dist/esm/generator/OperationsGenerator.js.map +1 -1
  52. package/dist/esm/generator/QueryModelsGenerator.js +4 -3
  53. package/dist/esm/generator/QueryModelsGenerator.js.map +1 -1
  54. package/dist/esm/generator/ServiceFnGenerator.js +6 -5
  55. package/dist/esm/generator/ServiceFnGenerator.js.map +1 -1
  56. package/dist/esm/model/GeneratorConfig.js.map +1 -1
  57. package/package.json +1 -1
  58. package/src/digester/TypeMapper.ts +72 -16
  59. package/src/generator/Generator.ts +15 -5
  60. package/src/generator/IndexGenerator.ts +27 -6
  61. package/src/generator/ModelsGenerator.ts +54 -0
  62. package/src/generator/NamingHelper.ts +26 -0
  63. package/src/generator/NavigationGenerator.ts +23 -5
  64. package/src/generator/OperationsGenerator.ts +25 -5
  65. package/src/generator/QueryModelsGenerator.ts +27 -3
  66. package/src/generator/ServiceFnGenerator.ts +23 -5
  67. package/src/model/GeneratorConfig.ts +22 -0
@@ -85,6 +85,48 @@ const sortTypesByDependency = <T extends ComplexTypeModel | EntityTypeModel>(
85
85
  return sorted
86
86
  }
87
87
 
88
+ /**
89
+ * Check if any property uses ODataSchema types.
90
+ */
91
+ const needsODataSchemaImport = (dataModel: DataModel): boolean => {
92
+ const checkProperties = (properties: ReadonlyArray<PropertyModel>): boolean =>
93
+ properties.some((p) => p.typeMapping.effectSchema.startsWith("ODataSchema."))
94
+
95
+ for (const type of dataModel.entityTypes.values()) {
96
+ if (checkProperties(type.properties)) return true
97
+ }
98
+ for (const type of dataModel.complexTypes.values()) {
99
+ if (checkProperties(type.properties)) return true
100
+ }
101
+ return false
102
+ }
103
+
104
+ /**
105
+ * Check which Effect type imports are needed based on tsType usage.
106
+ */
107
+ const getNeededEffectTypeImports = (dataModel: DataModel): Set<string> => {
108
+ const needed = new Set<string>()
109
+
110
+ const checkTsType = (tsType: string): void => {
111
+ if (tsType.startsWith("DateTime.")) needed.add("DateTime")
112
+ if (tsType.startsWith("BigDecimal.")) needed.add("BigDecimal")
113
+ if (tsType.startsWith("Duration.")) needed.add("Duration")
114
+ }
115
+
116
+ for (const type of dataModel.entityTypes.values()) {
117
+ for (const prop of type.properties) {
118
+ checkTsType(prop.typeMapping.tsType)
119
+ }
120
+ }
121
+ for (const type of dataModel.complexTypes.values()) {
122
+ for (const prop of type.properties) {
123
+ checkTsType(prop.typeMapping.tsType)
124
+ }
125
+ }
126
+
127
+ return needed
128
+ }
129
+
88
130
  /**
89
131
  * Generate the Models.ts file content.
90
132
  *
@@ -106,6 +148,18 @@ export const generateModels = (dataModel: DataModel): string => {
106
148
  lines.push(` * @since 1.0.0`)
107
149
  lines.push(` */`)
108
150
  lines.push(`import * as Schema from "effect/Schema"`)
151
+
152
+ // Add Effect type imports if needed (for DateTime, BigDecimal, Duration)
153
+ const effectTypeImports = getNeededEffectTypeImports(dataModel)
154
+ for (const effectType of effectTypeImports) {
155
+ lines.push(`import type * as ${effectType} from "effect/${effectType}"`)
156
+ }
157
+
158
+ // Add ODataSchema import if needed
159
+ if (needsODataSchemaImport(dataModel)) {
160
+ lines.push(`import { ODataSchema } from "@odata-effect/odata-effect"`)
161
+ }
162
+
109
163
  lines.push(``)
110
164
 
111
165
  // Generate enum types (no dependencies, always first)
@@ -279,3 +279,29 @@ export const getOperationParameterNameWithOverrides = (
279
279
  // Fall back to default camelCase conversion
280
280
  return getPropertyName(odataParamName)
281
281
  }
282
+
283
+ // ============================================================================
284
+ // Import path utilities
285
+ // ============================================================================
286
+
287
+ /**
288
+ * Format a relative import path, optionally adding .js extension for ESM compatibility.
289
+ *
290
+ * @param moduleName - The module name (e.g., "Models", "Services")
291
+ * @param esmExtensions - Whether to add .js extension (default: true)
292
+ * @returns The formatted import path (e.g., "./Models.js" or "./Models")
293
+ *
294
+ * @example
295
+ * formatRelativeImport("Models", true) // "./Models.js"
296
+ * formatRelativeImport("Models", false) // "./Models"
297
+ *
298
+ * @since 1.0.0
299
+ * @category imports
300
+ */
301
+ export const formatRelativeImport = (
302
+ moduleName: string,
303
+ esmExtensions: boolean = true
304
+ ): string => {
305
+ const ext = esmExtensions ? ".js" : ""
306
+ return `./${moduleName}${ext}`
307
+ }
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import type { DataModel, EntityTypeModel } from "../model/DataModel.js"
25
25
  import type { ODataVersion } from "../parser/EdmxSchema.js"
26
- import { getClassName, toCamelCase } from "./NamingHelper.js"
26
+ import { formatRelativeImport, getClassName, toCamelCase } from "./NamingHelper.js"
27
27
 
28
28
  /**
29
29
  * Version-specific configuration.
@@ -166,15 +166,33 @@ const getDerivedTypes = (
166
166
  return derived
167
167
  }
168
168
 
169
+ /**
170
+ * Options for navigation generation.
171
+ *
172
+ * @since 1.0.0
173
+ * @category types
174
+ */
175
+ export interface NavigationGeneratorOptions {
176
+ /**
177
+ * Add .js extensions to relative imports for ESM compatibility.
178
+ * @default true
179
+ */
180
+ readonly esmExtensions?: boolean
181
+ }
182
+
169
183
  /**
170
184
  * Generate navigation builders.
171
185
  *
172
186
  * @since 1.0.0
173
187
  * @category generation
174
188
  */
175
- export const generateNavigations = (dataModel: DataModel): NavigationGenerationResult => {
189
+ export const generateNavigations = (
190
+ dataModel: DataModel,
191
+ options?: NavigationGeneratorOptions
192
+ ): NavigationGenerationResult => {
193
+ const esmExtensions = options?.esmExtensions ?? true
176
194
  const moduleName = getPathBuildersModuleName()
177
- const content = generatePathBuildersFile(dataModel)
195
+ const content = generatePathBuildersFile(dataModel, esmExtensions)
178
196
 
179
197
  return {
180
198
  navigationFiles: [{
@@ -187,7 +205,7 @@ export const generateNavigations = (dataModel: DataModel): NavigationGenerationR
187
205
  /**
188
206
  * Generate the PathBuilders.ts file.
189
207
  */
190
- const generatePathBuildersFile = (dataModel: DataModel): string => {
208
+ const generatePathBuildersFile = (dataModel: DataModel, esmExtensions: boolean): string => {
191
209
  const lines: Array<string> = []
192
210
  const versionConfig = getVersionConfig(dataModel.version)
193
211
 
@@ -235,7 +253,7 @@ const generatePathBuildersFile = (dataModel: DataModel): string => {
235
253
  const isLast = i === typesList.length - 1
236
254
  lines.push(` ${typesList[i]} as ${typesList[i]}Model${isLast ? "" : ","}`)
237
255
  }
238
- lines.push(`} from "./Models"`)
256
+ lines.push(`} from "${formatRelativeImport("Models", esmExtensions)}"`)
239
257
  lines.push(``)
240
258
  }
241
259
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { DataModel, OperationModel, OperationParameterModel } from "../model/DataModel.js"
10
10
  import type { ODataVersion } from "../parser/EdmxSchema.js"
11
- import { toPascalCase } from "./NamingHelper.js"
11
+ import { formatRelativeImport, toPascalCase } from "./NamingHelper.js"
12
12
 
13
13
  /**
14
14
  * Version-specific imports and identifiers.
@@ -73,13 +73,32 @@ export interface OperationsGenerationResult {
73
73
  readonly operationsFile: GeneratedOperationsFile | null
74
74
  }
75
75
 
76
+ /**
77
+ * Options for operations generation.
78
+ *
79
+ * @since 1.0.0
80
+ * @category types
81
+ */
82
+ export interface OperationsGeneratorOptions {
83
+ /**
84
+ * Add .js extensions to relative imports for ESM compatibility.
85
+ * @default true
86
+ */
87
+ readonly esmExtensions?: boolean
88
+ }
89
+
76
90
  /**
77
91
  * Generate the operations file.
78
92
  *
79
93
  * @since 1.0.0
80
94
  * @category generation
81
95
  */
82
- export const generateOperations = (dataModel: DataModel): OperationsGenerationResult => {
96
+ export const generateOperations = (
97
+ dataModel: DataModel,
98
+ options?: OperationsGeneratorOptions
99
+ ): OperationsGenerationResult => {
100
+ const esmExtensions = options?.esmExtensions ?? true
101
+
83
102
  // Get unbound operations only (bound operations are attached to entity services)
84
103
  const unboundOperations = Array.from(dataModel.operations.values()).filter((op) => !op.isBound)
85
104
 
@@ -87,7 +106,7 @@ export const generateOperations = (dataModel: DataModel): OperationsGenerationRe
87
106
  return { operationsFile: null }
88
107
  }
89
108
 
90
- const content = generateOperationsFile(unboundOperations, dataModel)
109
+ const content = generateOperationsFile(unboundOperations, dataModel, esmExtensions)
91
110
 
92
111
  return {
93
112
  operationsFile: {
@@ -174,7 +193,8 @@ const collectModelImports = (operations: ReadonlyArray<OperationModel>, dataMode
174
193
  */
175
194
  const generateOperationsFile = (
176
195
  operations: ReadonlyArray<OperationModel>,
177
- dataModel: DataModel
196
+ dataModel: DataModel,
197
+ esmExtensions: boolean
178
198
  ): string => {
179
199
  const lines: Array<string> = []
180
200
  const versionConfig = getVersionConfig(dataModel.version)
@@ -215,7 +235,7 @@ const generateOperationsFile = (
215
235
  const isLast = i === importsList.length - 1
216
236
  lines.push(` ${importsList[i]}${isLast ? "" : ","}`)
217
237
  }
218
- lines.push(`} from "./Models"`)
238
+ lines.push(`} from "${formatRelativeImport("Models", esmExtensions)}"`)
219
239
  lines.push(``)
220
240
  }
221
241
 
@@ -10,7 +10,27 @@ import type {
10
10
  NavigationPropertyModel,
11
11
  PropertyModel
12
12
  } from "../model/DataModel.js"
13
- import { getClassName, getQueryFactoryName, getQueryInstanceName, getQueryInterfaceName } from "./NamingHelper.js"
13
+ import {
14
+ formatRelativeImport,
15
+ getClassName,
16
+ getQueryFactoryName,
17
+ getQueryInstanceName,
18
+ getQueryInterfaceName
19
+ } from "./NamingHelper.js"
20
+
21
+ /**
22
+ * Options for query models generation.
23
+ *
24
+ * @since 1.0.0
25
+ * @category types
26
+ */
27
+ export interface QueryModelsGeneratorOptions {
28
+ /**
29
+ * Add .js extensions to relative imports for ESM compatibility.
30
+ * @default true
31
+ */
32
+ readonly esmExtensions?: boolean
33
+ }
14
34
 
15
35
  /**
16
36
  * Generate the QueryModels.ts file content.
@@ -18,7 +38,11 @@ import { getClassName, getQueryFactoryName, getQueryInstanceName, getQueryInterf
18
38
  * @since 1.0.0
19
39
  * @category generation
20
40
  */
21
- export const generateQueryModels = (dataModel: DataModel): string => {
41
+ export const generateQueryModels = (
42
+ dataModel: DataModel,
43
+ options?: QueryModelsGeneratorOptions
44
+ ): string => {
45
+ const esmExtensions = options?.esmExtensions ?? true
22
46
  const lines: Array<string> = []
23
47
 
24
48
  // Collect all query path types needed
@@ -48,7 +72,7 @@ export const generateQueryModels = (dataModel: DataModel): string => {
48
72
  if (usedEntityNames.length > 0) {
49
73
  lines.push(`import type {`)
50
74
  lines.push(` ${usedEntityNames.join(",\n ")}`)
51
- lines.push(`} from "./Models"`)
75
+ lines.push(`} from "${formatRelativeImport("Models", esmExtensions)}"`)
52
76
  }
53
77
  lines.push(``)
54
78
 
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import type { DataModel, EntitySetModel, EntityTypeModel } from "../model/DataModel.js"
10
10
  import type { ODataVersion } from "../parser/EdmxSchema.js"
11
- import { getEditableTypeName, getIdTypeName, getServiceClassName } from "./NamingHelper.js"
11
+ import { formatRelativeImport, getEditableTypeName, getIdTypeName, getServiceClassName } from "./NamingHelper.js"
12
12
 
13
13
  /**
14
14
  * Generated service file.
@@ -31,14 +31,32 @@ export interface ServiceGenerationResult {
31
31
  readonly servicesFile: GeneratedServiceFile
32
32
  }
33
33
 
34
+ /**
35
+ * Options for service generation.
36
+ *
37
+ * @since 1.0.0
38
+ * @category types
39
+ */
40
+ export interface ServiceGeneratorOptions {
41
+ /**
42
+ * Add .js extensions to relative imports for ESM compatibility.
43
+ * @default true
44
+ */
45
+ readonly esmExtensions?: boolean
46
+ }
47
+
34
48
  /**
35
49
  * Generate the Services.ts file using crud factory.
36
50
  *
37
51
  * @since 1.0.0
38
52
  * @category generation
39
53
  */
40
- export const generateServiceFns = (dataModel: DataModel): ServiceGenerationResult => {
41
- const content = generateServicesFile(dataModel)
54
+ export const generateServiceFns = (
55
+ dataModel: DataModel,
56
+ options?: ServiceGeneratorOptions
57
+ ): ServiceGenerationResult => {
58
+ const esmExtensions = options?.esmExtensions ?? true
59
+ const content = generateServicesFile(dataModel, esmExtensions)
42
60
  return {
43
61
  servicesFile: {
44
62
  fileName: "Services.ts",
@@ -50,7 +68,7 @@ export const generateServiceFns = (dataModel: DataModel): ServiceGenerationResul
50
68
  /**
51
69
  * Generate the Services.ts file content.
52
70
  */
53
- const generateServicesFile = (dataModel: DataModel): string => {
71
+ const generateServicesFile = (dataModel: DataModel, esmExtensions: boolean): string => {
54
72
  const lines: Array<string> = []
55
73
  const isV4 = dataModel.version === "V4"
56
74
  const crudImportPath = isV4
@@ -97,7 +115,7 @@ const generateServicesFile = (dataModel: DataModel): string => {
97
115
  const isLast = i === modelImports.length - 1
98
116
  lines.push(` ${modelImports[i]}${isLast ? "" : ","}`)
99
117
  }
100
- lines.push(`} from "./Models"`)
118
+ lines.push(`} from "${formatRelativeImport("Models", esmExtensions)}"`)
101
119
  lines.push(``)
102
120
 
103
121
  // Re-export types from the crud module
@@ -4,6 +4,28 @@
4
4
  * @since 1.0.0
5
5
  */
6
6
 
7
+ /**
8
+ * Generator options for code generation.
9
+ *
10
+ * @since 1.0.0
11
+ * @category config
12
+ */
13
+ export interface GeneratorOptions {
14
+ /**
15
+ * Add .js extensions to relative imports for ESM compatibility.
16
+ * Required for moduleResolution: node16/nodenext.
17
+ * Default: true (recommended for ESM projects)
18
+ *
19
+ * @default true
20
+ */
21
+ readonly esmExtensions?: boolean
22
+
23
+ /**
24
+ * Naming overrides for customizing generated type and property names.
25
+ */
26
+ readonly overrides?: NamingOverrides
27
+ }
28
+
7
29
  /**
8
30
  * Override configuration for property names.
9
31
  * Maps OData property names to TypeScript property names.