@odata-effect/odata-effect-generator 0.1.2 → 0.2.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 (58) hide show
  1. package/README.md +223 -5
  2. package/dist/cjs/Cli.js +6 -2
  3. package/dist/cjs/Cli.js.map +1 -1
  4. package/dist/cjs/generator/Generator.js +33 -15
  5. package/dist/cjs/generator/Generator.js.map +1 -1
  6. package/dist/cjs/generator/IndexGenerator.js +19 -73
  7. package/dist/cjs/generator/IndexGenerator.js.map +1 -1
  8. package/dist/cjs/generator/NamingHelper.js +4 -1
  9. package/dist/cjs/generator/NamingHelper.js.map +1 -1
  10. package/dist/cjs/generator/NavigationGenerator.js +338 -0
  11. package/dist/cjs/generator/NavigationGenerator.js.map +1 -0
  12. package/dist/cjs/generator/OperationsGenerator.js +384 -0
  13. package/dist/cjs/generator/OperationsGenerator.js.map +1 -0
  14. package/dist/cjs/generator/QueryModelsGenerator.js +2 -2
  15. package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -1
  16. package/dist/cjs/generator/ServiceFnGenerator.js +1 -1
  17. package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -1
  18. package/dist/cjs/generator/ServiceFnPromiseGenerator.js +2 -2
  19. package/dist/cjs/generator/ServiceFnPromiseGenerator.js.map +1 -1
  20. package/dist/dts/Cli.d.ts.map +1 -1
  21. package/dist/dts/generator/Generator.d.ts +2 -0
  22. package/dist/dts/generator/Generator.d.ts.map +1 -1
  23. package/dist/dts/generator/IndexGenerator.d.ts.map +1 -1
  24. package/dist/dts/generator/NamingHelper.d.ts.map +1 -1
  25. package/dist/dts/generator/NavigationGenerator.d.ts +55 -0
  26. package/dist/dts/generator/NavigationGenerator.d.ts.map +1 -0
  27. package/dist/dts/generator/OperationsGenerator.d.ts +50 -0
  28. package/dist/dts/generator/OperationsGenerator.d.ts.map +1 -0
  29. package/dist/esm/Cli.js +6 -2
  30. package/dist/esm/Cli.js.map +1 -1
  31. package/dist/esm/generator/Generator.js +33 -15
  32. package/dist/esm/generator/Generator.js.map +1 -1
  33. package/dist/esm/generator/IndexGenerator.js +19 -73
  34. package/dist/esm/generator/IndexGenerator.js.map +1 -1
  35. package/dist/esm/generator/NamingHelper.js +4 -1
  36. package/dist/esm/generator/NamingHelper.js.map +1 -1
  37. package/dist/esm/generator/NavigationGenerator.js +330 -0
  38. package/dist/esm/generator/NavigationGenerator.js.map +1 -0
  39. package/dist/esm/generator/OperationsGenerator.js +375 -0
  40. package/dist/esm/generator/OperationsGenerator.js.map +1 -0
  41. package/dist/esm/generator/QueryModelsGenerator.js +2 -2
  42. package/dist/esm/generator/QueryModelsGenerator.js.map +1 -1
  43. package/dist/esm/generator/ServiceFnGenerator.js +1 -1
  44. package/dist/esm/generator/ServiceFnGenerator.js.map +1 -1
  45. package/dist/esm/generator/ServiceFnPromiseGenerator.js +2 -2
  46. package/dist/esm/generator/ServiceFnPromiseGenerator.js.map +1 -1
  47. package/generator/NavigationGenerator/package.json +6 -0
  48. package/generator/OperationsGenerator/package.json +6 -0
  49. package/package.json +17 -1
  50. package/src/Cli.ts +8 -2
  51. package/src/generator/Generator.ts +43 -13
  52. package/src/generator/IndexGenerator.ts +21 -74
  53. package/src/generator/NamingHelper.ts +6 -1
  54. package/src/generator/NavigationGenerator.ts +451 -0
  55. package/src/generator/OperationsGenerator.ts +481 -0
  56. package/src/generator/QueryModelsGenerator.ts +2 -2
  57. package/src/generator/ServiceFnGenerator.ts +1 -1
  58. package/src/generator/ServiceFnPromiseGenerator.ts +2 -2
@@ -0,0 +1,481 @@
1
+ /**
2
+ * Generator for OData operations (FunctionImports, Functions, Actions).
3
+ *
4
+ * OData V2: FunctionImports
5
+ * OData V4: Functions (GET, no side effects) and Actions (POST, with side effects)
6
+ *
7
+ * @since 1.0.0
8
+ */
9
+ import type { DataModel, OperationModel } from "../model/DataModel.js"
10
+ import type { ODataVersion } from "../parser/EdmxSchema.js"
11
+ import { toCamelCase, toPascalCase } from "./NamingHelper.js"
12
+
13
+ /**
14
+ * Version-specific imports and identifiers.
15
+ */
16
+ interface VersionConfig {
17
+ readonly clientConfigTag: string
18
+ readonly clientConfigImport: string
19
+ readonly clientModule: string
20
+ }
21
+
22
+ const V2_CONFIG: VersionConfig = {
23
+ clientConfigTag: "ODataClientConfig",
24
+ clientConfigImport: "ODataClientConfig",
25
+ clientModule: "ODataClient"
26
+ }
27
+
28
+ const V4_CONFIG: VersionConfig = {
29
+ clientConfigTag: "ODataV4ClientConfig",
30
+ clientConfigImport: "ODataV4ClientConfig",
31
+ clientModule: "ODataV4Client"
32
+ }
33
+
34
+ const getVersionConfig = (version: ODataVersion): VersionConfig => version === "V4" ? V4_CONFIG : V2_CONFIG
35
+
36
+ /**
37
+ * Get the function name for an operation.
38
+ *
39
+ * @since 1.0.0
40
+ * @category naming
41
+ */
42
+ export const getOperationFunctionName = (operation: OperationModel): string => {
43
+ return toCamelCase(operation.name)
44
+ }
45
+
46
+ /**
47
+ * Get the operations module name.
48
+ *
49
+ * @since 1.0.0
50
+ * @category naming
51
+ */
52
+ export const getOperationsModuleName = (): string => "Operations"
53
+
54
+ /**
55
+ * Generated operations file.
56
+ *
57
+ * @since 1.0.0
58
+ * @category types
59
+ */
60
+ export interface GeneratedOperationsFile {
61
+ readonly fileName: string
62
+ readonly content: string
63
+ }
64
+
65
+ /**
66
+ * Result of operations generation.
67
+ *
68
+ * @since 1.0.0
69
+ * @category types
70
+ */
71
+ export interface OperationsGenerationResult {
72
+ readonly operationsFile: GeneratedOperationsFile | null
73
+ }
74
+
75
+ /**
76
+ * Generate the operations file.
77
+ *
78
+ * @since 1.0.0
79
+ * @category generation
80
+ */
81
+ export const generateOperations = (dataModel: DataModel): OperationsGenerationResult => {
82
+ // Get unbound operations only (bound operations are attached to entity services)
83
+ const unboundOperations = Array.from(dataModel.operations.values()).filter((op) => !op.isBound)
84
+
85
+ if (unboundOperations.length === 0) {
86
+ return { operationsFile: null }
87
+ }
88
+
89
+ const content = generateOperationsFile(unboundOperations, dataModel)
90
+
91
+ return {
92
+ operationsFile: {
93
+ fileName: `${getOperationsModuleName()}.ts`,
94
+ content
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Determine if an operation returns an entity or complex type that needs a schema.
101
+ */
102
+ const returnsModelType = (operation: OperationModel, dataModel: DataModel): boolean => {
103
+ if (!operation.returnType) return false
104
+
105
+ const { odataType } = operation.returnType
106
+ // Remove Collection() wrapper if present
107
+ const baseType = odataType.replace(/^Collection\(|\)$/g, "")
108
+ // Get the type name (may be fully qualified)
109
+ const typeName = baseType.includes(".") ? baseType.split(".").pop() ?? baseType : baseType
110
+
111
+ // Check if it's an entity or complex type
112
+ for (const [fqName] of dataModel.entityTypes) {
113
+ if (fqName.endsWith(`.${typeName}`) || fqName === typeName) return true
114
+ }
115
+ for (const [fqName] of dataModel.complexTypes) {
116
+ if (fqName.endsWith(`.${typeName}`) || fqName === typeName) return true
117
+ }
118
+
119
+ return false
120
+ }
121
+
122
+ /**
123
+ * Get the return type's TypeScript type name.
124
+ */
125
+ const getReturnTypeName = (operation: OperationModel): string => {
126
+ if (!operation.returnType) return "void"
127
+
128
+ const { typeMapping, isCollection } = operation.returnType
129
+ const baseType = typeMapping.tsType
130
+
131
+ return isCollection ? `ReadonlyArray<${baseType}>` : baseType
132
+ }
133
+
134
+ /**
135
+ * Collect all model types that need to be imported.
136
+ */
137
+ const collectModelImports = (operations: ReadonlyArray<OperationModel>, dataModel: DataModel): Set<string> => {
138
+ const imports = new Set<string>()
139
+
140
+ const isModelType = (typeName: string): boolean => {
141
+ for (const [fqName] of dataModel.entityTypes) {
142
+ if (fqName.endsWith(`.${typeName}`) || fqName === typeName) return true
143
+ }
144
+ for (const [fqName] of dataModel.complexTypes) {
145
+ if (fqName.endsWith(`.${typeName}`) || fqName === typeName) return true
146
+ }
147
+ return false
148
+ }
149
+
150
+ for (const operation of operations) {
151
+ // Check return type
152
+ if (operation.returnType) {
153
+ const typeName = operation.returnType.typeMapping.tsType
154
+ if (isModelType(typeName)) {
155
+ imports.add(typeName)
156
+ }
157
+ }
158
+
159
+ // Check parameter types
160
+ for (const param of operation.parameters) {
161
+ const typeName = param.typeMapping.tsType
162
+ if (isModelType(typeName)) {
163
+ imports.add(typeName)
164
+ }
165
+ }
166
+ }
167
+
168
+ return imports
169
+ }
170
+
171
+ /**
172
+ * Generate the operations file content.
173
+ */
174
+ const generateOperationsFile = (
175
+ operations: ReadonlyArray<OperationModel>,
176
+ dataModel: DataModel
177
+ ): string => {
178
+ const lines: Array<string> = []
179
+ const versionConfig = getVersionConfig(dataModel.version)
180
+ const isV4 = dataModel.version === "V4"
181
+
182
+ // Header and imports
183
+ lines.push(`/**`)
184
+ lines.push(` * OData operations for ${dataModel.serviceName} OData ${dataModel.version}.`)
185
+ lines.push(` * Generated by odata-effect-gen.`)
186
+ lines.push(` *`)
187
+ lines.push(` * @since 1.0.0`)
188
+ lines.push(` */`)
189
+ lines.push(`import * as Effect from "effect/Effect"`)
190
+ lines.push(`import { HttpClient } from "@effect/platform"`)
191
+ lines.push(`import type * as HttpClientError from "@effect/platform/HttpClientError"`)
192
+ lines.push(``)
193
+
194
+ // Import Operations module
195
+ lines.push(`import * as ODataOps from "@odata-effect/odata-effect/Operations"`)
196
+ lines.push(`import {`)
197
+ lines.push(` ${versionConfig.clientConfigImport}`)
198
+ lines.push(`} from "@odata-effect/odata-effect/${versionConfig.clientModule}"`)
199
+ lines.push(`import {`)
200
+ if (!isV4) {
201
+ lines.push(` type SapError,`)
202
+ }
203
+ lines.push(` type ODataError,`)
204
+ lines.push(` type ParseError`)
205
+ lines.push(`} from "@odata-effect/odata-effect/Errors"`)
206
+ lines.push(``)
207
+
208
+ // Import model types if needed
209
+ const modelImports = collectModelImports(operations, dataModel)
210
+ if (modelImports.size > 0) {
211
+ lines.push(`import {`)
212
+ const importsList = Array.from(modelImports)
213
+ for (let i = 0; i < importsList.length; i++) {
214
+ const isLast = i === importsList.length - 1
215
+ lines.push(` ${importsList[i]}${isLast ? "" : ","}`)
216
+ }
217
+ lines.push(`} from "./Models"`)
218
+ lines.push(``)
219
+ }
220
+
221
+ // Error type
222
+ lines.push(`// ============================================================================`)
223
+ lines.push(`// Types`)
224
+ lines.push(`// ============================================================================`)
225
+ lines.push(``)
226
+ lines.push(`/**`)
227
+ lines.push(` * Error type for operation calls.`)
228
+ lines.push(` *`)
229
+ lines.push(` * @since 1.0.0`)
230
+ lines.push(` * @category errors`)
231
+ lines.push(` */`)
232
+ lines.push(`export type OperationsError =`)
233
+ lines.push(` | HttpClientError.HttpClientError`)
234
+ lines.push(` | ParseError`)
235
+ if (!isV4) {
236
+ lines.push(` | SapError`)
237
+ }
238
+ lines.push(` | ODataError`)
239
+ lines.push(``)
240
+
241
+ // Dependencies type
242
+ lines.push(`/**`)
243
+ lines.push(` * Dependencies required for operation calls.`)
244
+ lines.push(` *`)
245
+ lines.push(` * @since 1.0.0`)
246
+ lines.push(` * @category context`)
247
+ lines.push(` */`)
248
+ lines.push(`export type OperationsContext = ${versionConfig.clientConfigTag} | HttpClient.HttpClient`)
249
+ lines.push(``)
250
+
251
+ // Generate each operation
252
+ lines.push(`// ============================================================================`)
253
+ lines.push(`// Operations`)
254
+ lines.push(`// ============================================================================`)
255
+ lines.push(``)
256
+
257
+ for (const operation of operations) {
258
+ if (isV4) {
259
+ generateV4Operation(lines, operation, dataModel)
260
+ } else {
261
+ generateV2FunctionImport(lines, operation, dataModel)
262
+ }
263
+ lines.push(``)
264
+ }
265
+
266
+ return lines.join("\n")
267
+ }
268
+
269
+ /**
270
+ * Generate parameter interface for an operation.
271
+ */
272
+ const generateParameterType = (
273
+ operation: OperationModel
274
+ ): string | null => {
275
+ if (operation.parameters.length === 0) return null
276
+
277
+ const typeName = `${toPascalCase(operation.name)}Params`
278
+ return typeName
279
+ }
280
+
281
+ /**
282
+ * Generate parameter interface definition.
283
+ */
284
+ const generateParameterInterface = (
285
+ lines: Array<string>,
286
+ operation: OperationModel
287
+ ): void => {
288
+ if (operation.parameters.length === 0) return
289
+
290
+ const typeName = `${toPascalCase(operation.name)}Params`
291
+
292
+ lines.push(`/**`)
293
+ lines.push(` * Parameters for ${operation.odataName} operation.`)
294
+ lines.push(` *`)
295
+ lines.push(` * @since 1.0.0`)
296
+ lines.push(` * @category params`)
297
+ lines.push(` */`)
298
+ lines.push(`export interface ${typeName} {`)
299
+
300
+ for (const param of operation.parameters) {
301
+ const optionalMark = param.isNullable ? "?" : ""
302
+ const tsType = param.isCollection
303
+ ? `ReadonlyArray<${param.typeMapping.tsType}>`
304
+ : param.typeMapping.tsType
305
+ lines.push(` readonly ${param.name}${optionalMark}: ${tsType}`)
306
+ }
307
+
308
+ lines.push(`}`)
309
+ lines.push(``)
310
+ }
311
+
312
+ /**
313
+ * Generate V2 FunctionImport function.
314
+ */
315
+ const generateV2FunctionImport = (
316
+ lines: Array<string>,
317
+ operation: OperationModel,
318
+ dataModel: DataModel
319
+ ): void => {
320
+ const fnName = getOperationFunctionName(operation)
321
+ const paramsType = generateParameterType(operation)
322
+ const returnType = getReturnTypeName(operation)
323
+ const returnsModel = returnsModelType(operation, dataModel)
324
+
325
+ // Generate parameter interface if needed
326
+ generateParameterInterface(lines, operation)
327
+
328
+ lines.push(`/**`)
329
+ lines.push(` * Execute the ${operation.odataName} function import.`)
330
+ lines.push(` *`)
331
+ lines.push(` * @since 1.0.0`)
332
+ lines.push(` * @category operations`)
333
+ lines.push(` */`)
334
+
335
+ // Function signature
336
+ if (paramsType) {
337
+ lines.push(`export const ${fnName} = (`)
338
+ lines.push(` params: ${paramsType}`)
339
+ lines.push(`): Effect.Effect<`)
340
+ } else {
341
+ lines.push(`export const ${fnName} = (): Effect.Effect<`)
342
+ }
343
+
344
+ lines.push(` ${returnType},`)
345
+ lines.push(` OperationsError,`)
346
+ lines.push(` OperationsContext`)
347
+ lines.push(`> =>`)
348
+ lines.push(` Effect.gen(function*() {`)
349
+ lines.push(` const config = yield* ${V2_CONFIG.clientConfigImport}`)
350
+ lines.push(` const client = yield* HttpClient.HttpClient`)
351
+ lines.push(``)
352
+
353
+ // Build parameters object for the function import URL
354
+ 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(` }`)
360
+ lines.push(``)
361
+ }
362
+
363
+ const paramsArg = paramsType ? "parameters" : "undefined"
364
+
365
+ // Determine the execute function to use
366
+ if (!operation.returnType) {
367
+ lines.push(` return yield* ODataOps.executeFunctionImportVoid(client, config, "${operation.odataName}", ${paramsArg})`)
368
+ } else if (operation.returnType.isCollection) {
369
+ if (returnsModel) {
370
+ lines.push(` return yield* ODataOps.executeFunctionImportCollection(client, config, "${operation.odataName}", ${operation.returnType.typeMapping.tsType}, ${paramsArg})`)
371
+ } else {
372
+ lines.push(` return yield* ODataOps.executeFunctionImportCollection(client, config, "${operation.odataName}", ${operation.returnType.typeMapping.effectSchema}, ${paramsArg})`)
373
+ }
374
+ } else if (returnsModel) {
375
+ lines.push(` return yield* ODataOps.executeFunctionImportEntity(client, config, "${operation.odataName}", ${operation.returnType.typeMapping.tsType}, ${paramsArg})`)
376
+ } else {
377
+ // Primitive return
378
+ const propertyName = operation.odataName
379
+ lines.push(` return yield* ODataOps.executeFunctionImportPrimitive(client, config, "${operation.odataName}", "${propertyName}", ${operation.returnType.typeMapping.effectSchema}, ${paramsArg})`)
380
+ }
381
+
382
+ lines.push(` })`)
383
+ }
384
+
385
+ /**
386
+ * Generate V4 Function or Action.
387
+ */
388
+ const generateV4Operation = (
389
+ lines: Array<string>,
390
+ operation: OperationModel,
391
+ dataModel: DataModel
392
+ ): void => {
393
+ const fnName = getOperationFunctionName(operation)
394
+ const paramsType = generateParameterType(operation)
395
+ const returnType = getReturnTypeName(operation)
396
+ const returnsModel = returnsModelType(operation, dataModel)
397
+ const isAction = operation.type === "Action"
398
+
399
+ // Generate parameter interface if needed
400
+ generateParameterInterface(lines, operation)
401
+
402
+ lines.push(`/**`)
403
+ lines.push(` * Execute the ${operation.odataName} ${operation.type.toLowerCase()}.`)
404
+ lines.push(` *`)
405
+ lines.push(` * @since 1.0.0`)
406
+ lines.push(` * @category operations`)
407
+ lines.push(` */`)
408
+
409
+ // Function signature
410
+ if (paramsType) {
411
+ lines.push(`export const ${fnName} = (`)
412
+ lines.push(` params: ${paramsType}`)
413
+ lines.push(`): Effect.Effect<`)
414
+ } else {
415
+ lines.push(`export const ${fnName} = (): Effect.Effect<`)
416
+ }
417
+
418
+ lines.push(` ${returnType},`)
419
+ lines.push(` OperationsError,`)
420
+ lines.push(` OperationsContext`)
421
+ lines.push(`> =>`)
422
+ lines.push(` Effect.gen(function*() {`)
423
+ lines.push(` const config = yield* ${V4_CONFIG.clientConfigImport}`)
424
+ lines.push(` const client = yield* HttpClient.HttpClient`)
425
+ lines.push(``)
426
+
427
+ // Build URL for functions or action
428
+ if (!isAction) {
429
+ // V4 Functions use GET with parameters in URL
430
+ if (paramsType) {
431
+ lines.push(` const parameters: ODataOps.OperationParameters = {`)
432
+ for (const param of operation.parameters) {
433
+ lines.push(` ${param.name}: params.${param.name},`)
434
+ }
435
+ lines.push(` }`)
436
+ lines.push(` const url = ODataOps.buildV4FunctionUrl("${operation.odataName}", parameters)`)
437
+ } else {
438
+ lines.push(` const url = ODataOps.buildV4FunctionUrl("${operation.odataName}")`)
439
+ }
440
+ lines.push(``)
441
+
442
+ // Determine the execute function to use
443
+ if (!operation.returnType) {
444
+ lines.push(` return yield* ODataOps.executeV4FunctionVoid(client, config, url)`)
445
+ } else if (operation.returnType.isCollection) {
446
+ if (returnsModel) {
447
+ lines.push(` return yield* ODataOps.executeV4FunctionCollection(client, config, url, ${operation.returnType.typeMapping.tsType})`)
448
+ } else {
449
+ lines.push(` return yield* ODataOps.executeV4FunctionCollection(client, config, url, ${operation.returnType.typeMapping.effectSchema})`)
450
+ }
451
+ } else if (returnsModel) {
452
+ lines.push(` return yield* ODataOps.executeV4FunctionEntity(client, config, url, ${operation.returnType.typeMapping.tsType})`)
453
+ } else {
454
+ // Primitive return
455
+ lines.push(` return yield* ODataOps.executeV4FunctionPrimitive(client, config, url, ${operation.returnType.typeMapping.effectSchema})`)
456
+ }
457
+ } else {
458
+ // V4 Actions use POST with body
459
+ lines.push(` const url = "${operation.odataName}"`)
460
+ lines.push(``)
461
+
462
+ const bodyArg = paramsType ? "params" : "undefined"
463
+
464
+ if (!operation.returnType) {
465
+ lines.push(` return yield* ODataOps.executeV4ActionVoid(client, config, url, ${bodyArg})`)
466
+ } else if (operation.returnType.isCollection) {
467
+ if (returnsModel) {
468
+ lines.push(` return yield* ODataOps.executeV4ActionCollection(client, config, url, ${operation.returnType.typeMapping.tsType}, ${bodyArg})`)
469
+ } else {
470
+ lines.push(` return yield* ODataOps.executeV4ActionCollection(client, config, url, ${operation.returnType.typeMapping.effectSchema}, ${bodyArg})`)
471
+ }
472
+ } else if (returnsModel) {
473
+ lines.push(` return yield* ODataOps.executeV4ActionEntity(client, config, url, ${operation.returnType.typeMapping.tsType}, ${bodyArg})`)
474
+ } else {
475
+ // Primitive return - use entity with schema
476
+ lines.push(` return yield* ODataOps.executeV4FunctionPrimitive(client, config, url, ${operation.returnType.typeMapping.effectSchema})`)
477
+ }
478
+ }
479
+
480
+ lines.push(` })`)
481
+ }
@@ -35,7 +35,7 @@ export const generateQueryModels = (dataModel: DataModel): string => {
35
35
  lines.push(` ${Array.from(queryPathTypes).join(",\n ")},`)
36
36
  lines.push(` createQueryBuilder,`)
37
37
  lines.push(` type QueryBuilder`)
38
- lines.push(`} from "@odata-effect/odata-effect"`)
38
+ lines.push(`} from "@odata-effect/odata-effect/QueryBuilder"`)
39
39
 
40
40
  // Import entity types that have entity sets (for QueryBuilder generics)
41
41
  const usedEntityNames: Array<string> = []
@@ -48,7 +48,7 @@ export const generateQueryModels = (dataModel: DataModel): string => {
48
48
  if (usedEntityNames.length > 0) {
49
49
  lines.push(`import type {`)
50
50
  lines.push(` ${usedEntityNames.join(",\n ")}`)
51
- lines.push(`} from "./Models.js"`)
51
+ lines.push(`} from "./Models"`)
52
52
  }
53
53
  lines.push(``)
54
54
 
@@ -133,7 +133,7 @@ const generateEntityServiceFnFile = (
133
133
  lines.push(` type ${idTypeName},`)
134
134
  }
135
135
  lines.push(` ${editableName}`)
136
- lines.push(`} from "./Models.js"`)
136
+ lines.push(`} from "./Models"`)
137
137
  lines.push(``)
138
138
 
139
139
  // Import OData infrastructure (version-specific) using subpath imports for tree-shaking
@@ -130,7 +130,7 @@ const generatePromiseServiceFile = (
130
130
  lines.push(` * @since 1.0.0`)
131
131
  lines.push(` */`)
132
132
  lines.push(`import { ${versionConfig.runtimeImport} } from "@odata-effect/odata-effect-promise/Runtime"`)
133
- lines.push(`import * as ${serviceClassName} from "./${serviceClassName}.js"`)
133
+ lines.push(`import * as ${serviceClassName} from "./${serviceClassName}"`)
134
134
  lines.push(``)
135
135
 
136
136
  // Import models (type-only since they're only used in type annotations)
@@ -140,7 +140,7 @@ const generatePromiseServiceFile = (
140
140
  lines.push(` ${idTypeName},`)
141
141
  }
142
142
  lines.push(` ${editableName}`)
143
- lines.push(`} from "./Models.js"`)
143
+ lines.push(`} from "./Models"`)
144
144
  lines.push(``)
145
145
 
146
146
  // Query options import (using subpath imports for tree-shaking)