@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
package/src/Cli.ts CHANGED
@@ -39,17 +39,22 @@ const force = Options.boolean("force").pipe(
39
39
  Options.withDescription("Overwrite existing files")
40
40
  )
41
41
 
42
+ const filesOnly = Options.boolean("files-only").pipe(
43
+ Options.withDefault(false),
44
+ Options.withDescription("Generate only source files (no package.json, tsconfig, etc.) directly in output-dir")
45
+ )
46
+
42
47
  // ============================================================================
43
48
  // Generate Command
44
49
  // ============================================================================
45
50
 
46
51
  const generateCommand = Command.make(
47
52
  "generate",
48
- { metadataPath, outputDir, serviceName, packageName, force }
53
+ { metadataPath, outputDir, serviceName, packageName, force, filesOnly }
49
54
  ).pipe(
50
55
  Command.withDescription("Generate Effect OData client from metadata"),
51
56
  Command.withHandler((
52
- { force: forceOverwrite, metadataPath: metaPath, outputDir: outDir, packageName: pkgName, serviceName: svcName }
57
+ { force: forceOverwrite, metadataPath: metaPath, outputDir: outDir, packageName: pkgName, serviceName: svcName, filesOnly: onlyFiles }
53
58
  ) =>
54
59
  Effect.gen(function*() {
55
60
  const fs = yield* FileSystem.FileSystem
@@ -85,6 +90,7 @@ const generateCommand = Command.make(
85
90
  const config = {
86
91
  outputDir: outDir,
87
92
  force: forceOverwrite,
93
+ filesOnly: onlyFiles,
88
94
  ...(svcName._tag === "Some" ? { serviceName: svcName.value } : {}),
89
95
  ...(pkgName._tag === "Some" ? { packageName: pkgName.value } : {})
90
96
  }
@@ -10,6 +10,8 @@ import * as Schema from "effect/Schema"
10
10
  import type { DataModel } from "../model/DataModel.js"
11
11
  import { generateIndex } from "./IndexGenerator.js"
12
12
  import { generateModels } from "./ModelsGenerator.js"
13
+ import { generateNavigations } from "./NavigationGenerator.js"
14
+ import { generateOperations } from "./OperationsGenerator.js"
13
15
  import {
14
16
  generatePackageJson,
15
17
  generateTsconfig,
@@ -34,6 +36,8 @@ export interface GeneratorConfig {
34
36
  readonly packageName?: string
35
37
  readonly serviceName?: string
36
38
  readonly force?: boolean
39
+ /** Generate only source files directly in outputDir (no package.json, tsconfig, src/ subdirectory) */
40
+ readonly filesOnly?: boolean
37
41
  }
38
42
 
39
43
  /**
@@ -75,6 +79,10 @@ export const generate = (
75
79
  const outputDir = config.outputDir
76
80
  const serviceName = config.serviceName ?? dataModel.serviceName
77
81
  const packageName = config.packageName ?? `@template/${serviceName.toLowerCase()}-effect`
82
+ const filesOnly = config.filesOnly ?? false
83
+
84
+ // When filesOnly is true, output directly to outputDir; otherwise use outputDir/src
85
+ const sourceDir = filesOnly ? outputDir : path.join(outputDir, "src")
78
86
 
79
87
  const packageConfig: PackageConfig = {
80
88
  packageName,
@@ -87,32 +95,52 @@ export const generate = (
87
95
  // Generate Promise-based service function files
88
96
  const promiseServiceResult = generatePromiseServiceFns(dataModel)
89
97
 
90
- // Generate all files
91
- const files: Array<GeneratedFile> = [
92
- // Source files
98
+ // Generate operations file (FunctionImports, Functions, Actions)
99
+ const operationsResult = generateOperations(dataModel)
100
+
101
+ // Generate navigation builders
102
+ const navigationResult = generateNavigations(dataModel)
103
+
104
+ // Generate source files
105
+ const sourceFiles: Array<GeneratedFile> = [
93
106
  {
94
- path: path.join(outputDir, "src", "Models.ts"),
107
+ path: path.join(sourceDir, "Models.ts"),
95
108
  content: generateModels(dataModel)
96
109
  },
97
110
  {
98
- path: path.join(outputDir, "src", "QueryModels.ts"),
111
+ path: path.join(sourceDir, "QueryModels.ts"),
99
112
  content: generateQueryModels(dataModel)
100
113
  },
101
114
  // Individual entity service function files (tree-shakable)
102
115
  ...serviceResult.entityServices.map((svc) => ({
103
- path: path.join(outputDir, "src", svc.fileName),
116
+ path: path.join(sourceDir, svc.fileName),
104
117
  content: svc.content
105
118
  })),
106
119
  // Promise-based entity service function files
107
120
  ...promiseServiceResult.entityServices.map((svc) => ({
108
- path: path.join(outputDir, "src", svc.fileName),
121
+ path: path.join(sourceDir, svc.fileName),
109
122
  content: svc.content
110
123
  })),
124
+ // Operations file (only if there are unbound operations)
125
+ ...(operationsResult.operationsFile
126
+ ? [{
127
+ path: path.join(sourceDir, operationsResult.operationsFile.fileName),
128
+ content: operationsResult.operationsFile.content
129
+ }]
130
+ : []),
131
+ // Navigation builder files
132
+ ...navigationResult.navigationFiles.map((nav) => ({
133
+ path: path.join(sourceDir, nav.fileName),
134
+ content: nav.content
135
+ })),
111
136
  {
112
- path: path.join(outputDir, "src", "index.ts"),
137
+ path: path.join(sourceDir, "index.ts"),
113
138
  content: generateIndex(dataModel)
114
- },
115
- // Package configuration files
139
+ }
140
+ ]
141
+
142
+ // Package configuration files (only when not filesOnly)
143
+ const packageFiles: Array<GeneratedFile> = filesOnly ? [] : [
116
144
  {
117
145
  path: path.join(outputDir, "package.json"),
118
146
  content: generatePackageJson(dataModel, packageConfig)
@@ -139,11 +167,13 @@ export const generate = (
139
167
  }
140
168
  ]
141
169
 
142
- // Create output directories
143
- yield* fs.makeDirectory(path.join(outputDir, "src"), { recursive: true }).pipe(
170
+ const files = [...sourceFiles, ...packageFiles]
171
+
172
+ // Create output directory
173
+ yield* fs.makeDirectory(sourceDir, { recursive: true }).pipe(
144
174
  Effect.mapError((error) =>
145
175
  new GeneratorError({
146
- message: `Failed to create output directory: ${outputDir}/src`,
176
+ message: `Failed to create output directory: ${sourceDir}`,
147
177
  cause: error
148
178
  })
149
179
  )
@@ -12,6 +12,8 @@ import {
12
12
  getQueryInterfaceName,
13
13
  getServiceClassName
14
14
  } from "./NamingHelper.js"
15
+ import { getPathBuildersModuleName } from "./NavigationGenerator.js"
16
+ import { getOperationsModuleName } from "./OperationsGenerator.js"
15
17
  import { getPromiseServiceName } from "./ServiceFnPromiseGenerator.js"
16
18
 
17
19
  /**
@@ -63,84 +65,14 @@ export const generateIndex = (dataModel: DataModel): string => {
63
65
  lines.push(` ${modelExports[i]}${isLast ? "" : ","}`)
64
66
  }
65
67
 
66
- lines.push(`} from "./Models.js"`)
67
- lines.push(``)
68
-
69
- // Re-export OData infrastructure
70
- const isV4 = dataModel.version === "V4"
71
- lines.push(`// Re-export OData infrastructure from @odata-effect/odata-effect`)
72
- lines.push(`export {`)
73
- lines.push(` // Errors`)
74
- if (!isV4) {
75
- lines.push(` SapErrorDetail,`)
76
- lines.push(` SapErrorResolution,`)
77
- lines.push(` SapApplication,`)
78
- lines.push(` SapInnerError,`)
79
- lines.push(` SapErrorMessage,`)
80
- lines.push(` SapErrorBody,`)
81
- lines.push(` SapErrorResponse,`)
82
- lines.push(` SapError,`)
83
- }
84
- lines.push(` ODataError,`)
85
- lines.push(` EntityNotFoundError,`)
86
- lines.push(` ParseError,`)
87
- if (isV4) {
88
- // V4 exports
89
- lines.push(` // OData V4`)
90
- lines.push(` ODataV4,`)
91
- lines.push(` ODataV4CollectionResponse,`)
92
- lines.push(` ODataV4ValueResponse,`)
93
- lines.push(` ODataV4Annotations,`)
94
- lines.push(` ODataV4ClientConfig,`)
95
- lines.push(` buildEntityPathV4,`)
96
- lines.push(` type ODataV4QueryOptions,`)
97
- lines.push(` type ODataV4RequestOptions,`)
98
- lines.push(` type ODataV4ClientConfigService,`)
99
- lines.push(` type PagedResultV4,`)
100
- } else {
101
- // V2 exports
102
- lines.push(` // OData V2`)
103
- lines.push(` OData,`)
104
- lines.push(` ODataSingleResponse,`)
105
- lines.push(` ODataCollectionResponse,`)
106
- lines.push(` ODataCollectionResponseWithMeta,`)
107
- lines.push(` ODataValueResponse,`)
108
- lines.push(` EntityMetadata,`)
109
- lines.push(` MediaMetadata,`)
110
- lines.push(` DeferredContent,`)
111
- lines.push(` ODataClientConfig,`)
112
- lines.push(` buildEntityPath,`)
113
- lines.push(` DEFAULT_HEADERS,`)
114
- lines.push(` MERGE_HEADERS,`)
115
- lines.push(` type ODataQueryOptions,`)
116
- lines.push(` type ODataRequestOptions,`)
117
- lines.push(` type ODataClientConfigService,`)
118
- lines.push(` type PagedResult,`)
119
- }
120
- lines.push(` // Query Builder`)
121
- lines.push(` FilterExpression,`)
122
- lines.push(` StringPath,`)
123
- lines.push(` NumberPath,`)
124
- lines.push(` BooleanPath,`)
125
- lines.push(` DateTimePath,`)
126
- lines.push(` EntityPath,`)
127
- lines.push(` CollectionPath,`)
128
- lines.push(` QueryBuilder,`)
129
- lines.push(` createQueryPaths,`)
130
- lines.push(` createQueryBuilder,`)
131
- lines.push(` type FieldToPath,`)
132
- lines.push(` type QueryPaths,`)
133
- lines.push(` type SelectableKeys,`)
134
- lines.push(` type ExpandableKeys,`)
135
- lines.push(` type BuiltQuery`)
136
- lines.push(`} from "@odata-effect/odata-effect"`)
68
+ lines.push(`} from "./Models"`)
137
69
  lines.push(``)
138
70
 
139
71
  // Individual Entity Services (tree-shakable module namespace re-exports)
140
72
  lines.push(`// Individual Entity Services (tree-shakable)`)
141
73
  for (const entitySet of dataModel.entitySets.values()) {
142
74
  const serviceClassName = getServiceClassName(entitySet.name)
143
- lines.push(`export * as ${serviceClassName} from "./${serviceClassName}.js"`)
75
+ lines.push(`export * as ${serviceClassName} from "./${serviceClassName}"`)
144
76
  }
145
77
  lines.push(``)
146
78
 
@@ -148,10 +80,25 @@ export const generateIndex = (dataModel: DataModel): string => {
148
80
  lines.push(`// Promise-based Entity Services (for non-Effect environments)`)
149
81
  for (const entitySet of dataModel.entitySets.values()) {
150
82
  const promiseServiceName = getPromiseServiceName(entitySet.name)
151
- lines.push(`export * as ${promiseServiceName} from "./${promiseServiceName}.js"`)
83
+ lines.push(`export * as ${promiseServiceName} from "./${promiseServiceName}"`)
152
84
  }
153
85
  lines.push(``)
154
86
 
87
+ // Operations (FunctionImports, Functions, Actions) - only if there are unbound operations
88
+ const hasUnboundOperations = Array.from(dataModel.operations.values()).some((op) => !op.isBound)
89
+ if (hasUnboundOperations) {
90
+ lines.push(`// Operations (FunctionImports, Functions, Actions)`)
91
+ const operationsModuleName = getOperationsModuleName()
92
+ lines.push(`export * as ${operationsModuleName} from "./${operationsModuleName}"`)
93
+ lines.push(``)
94
+ }
95
+
96
+ // Path Builders (tree-shakable navigation)
97
+ lines.push(`// Path Builders (tree-shakable navigation)`)
98
+ const pathBuildersModuleName = getPathBuildersModuleName()
99
+ lines.push(`export * from "./${pathBuildersModuleName}"`)
100
+ lines.push(``)
101
+
155
102
  // Query Models
156
103
  lines.push(`// Query Models`)
157
104
  lines.push(`export {`)
@@ -183,7 +130,7 @@ export const generateIndex = (dataModel: DataModel): string => {
183
130
  lines.push(` ${queryExports[i]}${isLast ? "" : ","}`)
184
131
  }
185
132
 
186
- lines.push(`} from "./QueryModels.js"`)
133
+ lines.push(`} from "./QueryModels"`)
187
134
 
188
135
  return lines.join("\n")
189
136
  }
@@ -84,10 +84,15 @@ export const getServiceClassName = (entitySetName: string): string => {
84
84
  // Remove trailing 's' if present to singularize
85
85
  let singular = entitySetName
86
86
  if (singular.endsWith("ies")) {
87
+ // "Categories" -> "Category"
87
88
  singular = singular.slice(0, -3) + "y"
88
- } else if (singular.endsWith("es") && !singular.endsWith("ses")) {
89
+ } else if (singular.endsWith("xes") || singular.endsWith("ches") ||
90
+ singular.endsWith("shes") || singular.endsWith("sses") ||
91
+ singular.endsWith("zes")) {
92
+ // "Boxes" -> "Box", "Beaches" -> "Beach", etc.
89
93
  singular = singular.slice(0, -2)
90
94
  } else if (singular.endsWith("s") && !singular.endsWith("ss")) {
95
+ // "Products" -> "Product", "Airlines" -> "Airline"
91
96
  singular = singular.slice(0, -1)
92
97
  }
93
98
  return `${toPascalCase(singular)}Service`