@odata-effect/odata-effect-generator 0.1.2 → 0.3.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 (74) hide show
  1. package/README.md +232 -5
  2. package/dist/cjs/Cli.js +5 -1
  3. package/dist/cjs/Cli.js.map +1 -1
  4. package/dist/cjs/generator/Generator.js +35 -25
  5. package/dist/cjs/generator/Generator.js.map +1 -1
  6. package/dist/cjs/generator/IndexGenerator.js +29 -80
  7. package/dist/cjs/generator/IndexGenerator.js.map +1 -1
  8. package/dist/cjs/generator/ModelsGenerator.js +2 -5
  9. package/dist/cjs/generator/ModelsGenerator.js.map +1 -1
  10. package/dist/cjs/generator/NamingHelper.js +4 -1
  11. package/dist/cjs/generator/NamingHelper.js.map +1 -1
  12. package/dist/cjs/generator/NavigationGenerator.js +370 -0
  13. package/dist/cjs/generator/NavigationGenerator.js.map +1 -0
  14. package/dist/cjs/generator/OperationsGenerator.js +384 -0
  15. package/dist/cjs/generator/OperationsGenerator.js.map +1 -0
  16. package/dist/cjs/generator/QueryModelsGenerator.js +2 -2
  17. package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -1
  18. package/dist/cjs/generator/ServiceFnGenerator.js +102 -224
  19. package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -1
  20. package/dist/cjs/index.js +3 -3
  21. package/dist/dts/Cli.d.ts.map +1 -1
  22. package/dist/dts/generator/Generator.d.ts +2 -0
  23. package/dist/dts/generator/Generator.d.ts.map +1 -1
  24. package/dist/dts/generator/IndexGenerator.d.ts.map +1 -1
  25. package/dist/dts/generator/NamingHelper.d.ts.map +1 -1
  26. package/dist/dts/generator/NavigationGenerator.d.ts +55 -0
  27. package/dist/dts/generator/NavigationGenerator.d.ts.map +1 -0
  28. package/dist/dts/generator/OperationsGenerator.d.ts +50 -0
  29. package/dist/dts/generator/OperationsGenerator.d.ts.map +1 -0
  30. package/dist/dts/generator/ServiceFnGenerator.d.ts +10 -11
  31. package/dist/dts/generator/ServiceFnGenerator.d.ts.map +1 -1
  32. package/dist/dts/index.d.ts +4 -4
  33. package/dist/dts/index.d.ts.map +1 -1
  34. package/dist/esm/Cli.js +5 -1
  35. package/dist/esm/Cli.js.map +1 -1
  36. package/dist/esm/generator/Generator.js +35 -25
  37. package/dist/esm/generator/Generator.js.map +1 -1
  38. package/dist/esm/generator/IndexGenerator.js +29 -80
  39. package/dist/esm/generator/IndexGenerator.js.map +1 -1
  40. package/dist/esm/generator/ModelsGenerator.js +2 -5
  41. package/dist/esm/generator/ModelsGenerator.js.map +1 -1
  42. package/dist/esm/generator/NamingHelper.js +4 -1
  43. package/dist/esm/generator/NamingHelper.js.map +1 -1
  44. package/dist/esm/generator/NavigationGenerator.js +362 -0
  45. package/dist/esm/generator/NavigationGenerator.js.map +1 -0
  46. package/dist/esm/generator/OperationsGenerator.js +375 -0
  47. package/dist/esm/generator/OperationsGenerator.js.map +1 -0
  48. package/dist/esm/generator/QueryModelsGenerator.js +2 -2
  49. package/dist/esm/generator/QueryModelsGenerator.js.map +1 -1
  50. package/dist/esm/generator/ServiceFnGenerator.js +102 -224
  51. package/dist/esm/generator/ServiceFnGenerator.js.map +1 -1
  52. package/dist/esm/index.js +4 -4
  53. package/dist/esm/index.js.map +1 -1
  54. package/generator/NavigationGenerator/package.json +6 -0
  55. package/generator/OperationsGenerator/package.json +6 -0
  56. package/package.json +17 -9
  57. package/src/Cli.ts +15 -2
  58. package/src/generator/Generator.ts +44 -23
  59. package/src/generator/IndexGenerator.ts +29 -80
  60. package/src/generator/ModelsGenerator.ts +2 -5
  61. package/src/generator/NamingHelper.ts +8 -1
  62. package/src/generator/NavigationGenerator.ts +485 -0
  63. package/src/generator/OperationsGenerator.ts +507 -0
  64. package/src/generator/QueryModelsGenerator.ts +2 -2
  65. package/src/generator/ServiceFnGenerator.ts +117 -265
  66. package/src/index.ts +4 -4
  67. package/dist/cjs/generator/ServiceFnPromiseGenerator.js +0 -183
  68. package/dist/cjs/generator/ServiceFnPromiseGenerator.js.map +0 -1
  69. package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts +0 -40
  70. package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts.map +0 -1
  71. package/dist/esm/generator/ServiceFnPromiseGenerator.js +0 -175
  72. package/dist/esm/generator/ServiceFnPromiseGenerator.js.map +0 -1
  73. package/generator/ServiceFnPromiseGenerator/package.json +0 -6
  74. package/src/generator/ServiceFnPromiseGenerator.ts +0 -243
@@ -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,
@@ -21,7 +23,6 @@ import {
21
23
  } from "./PackageGenerator.js"
22
24
  import { generateQueryModels } from "./QueryModelsGenerator.js"
23
25
  import { generateServiceFns } from "./ServiceFnGenerator.js"
24
- import { generatePromiseServiceFns } from "./ServiceFnPromiseGenerator.js"
25
26
 
26
27
  /**
27
28
  * Generator configuration.
@@ -34,6 +35,8 @@ export interface GeneratorConfig {
34
35
  readonly packageName?: string
35
36
  readonly serviceName?: string
36
37
  readonly force?: boolean
38
+ /** Generate only source files directly in outputDir (no package.json, tsconfig, src/ subdirectory) */
39
+ readonly filesOnly?: boolean
37
40
  }
38
41
 
39
42
  /**
@@ -75,6 +78,10 @@ export const generate = (
75
78
  const outputDir = config.outputDir
76
79
  const serviceName = config.serviceName ?? dataModel.serviceName
77
80
  const packageName = config.packageName ?? `@template/${serviceName.toLowerCase()}-effect`
81
+ const filesOnly = config.filesOnly ?? false
82
+
83
+ // When filesOnly is true, output directly to outputDir; otherwise use outputDir/src
84
+ const sourceDir = filesOnly ? outputDir : path.join(outputDir, "src")
78
85
 
79
86
  const packageConfig: PackageConfig = {
80
87
  packageName,
@@ -84,35 +91,47 @@ export const generate = (
84
91
  // Generate tree-shakable service function files
85
92
  const serviceResult = generateServiceFns(dataModel)
86
93
 
87
- // Generate Promise-based service function files
88
- const promiseServiceResult = generatePromiseServiceFns(dataModel)
94
+ // Generate operations file (FunctionImports, Functions, Actions)
95
+ const operationsResult = generateOperations(dataModel)
96
+
97
+ // Generate navigation builders
98
+ const navigationResult = generateNavigations(dataModel)
89
99
 
90
- // Generate all files
91
- const files: Array<GeneratedFile> = [
92
- // Source files
100
+ // Generate source files
101
+ const sourceFiles: Array<GeneratedFile> = [
93
102
  {
94
- path: path.join(outputDir, "src", "Models.ts"),
103
+ path: path.join(sourceDir, "Models.ts"),
95
104
  content: generateModels(dataModel)
96
105
  },
97
106
  {
98
- path: path.join(outputDir, "src", "QueryModels.ts"),
107
+ path: path.join(sourceDir, "QueryModels.ts"),
99
108
  content: generateQueryModels(dataModel)
100
109
  },
101
- // Individual entity service function files (tree-shakable)
102
- ...serviceResult.entityServices.map((svc) => ({
103
- path: path.join(outputDir, "src", svc.fileName),
104
- content: svc.content
105
- })),
106
- // Promise-based entity service function files
107
- ...promiseServiceResult.entityServices.map((svc) => ({
108
- path: path.join(outputDir, "src", svc.fileName),
109
- content: svc.content
110
+ // Services file (all entity CRUD services in one file)
111
+ {
112
+ path: path.join(sourceDir, serviceResult.servicesFile.fileName),
113
+ content: serviceResult.servicesFile.content
114
+ },
115
+ // Operations file (only if there are unbound operations)
116
+ ...(operationsResult.operationsFile
117
+ ? [{
118
+ path: path.join(sourceDir, operationsResult.operationsFile.fileName),
119
+ content: operationsResult.operationsFile.content
120
+ }]
121
+ : []),
122
+ // Navigation builder files
123
+ ...navigationResult.navigationFiles.map((nav) => ({
124
+ path: path.join(sourceDir, nav.fileName),
125
+ content: nav.content
110
126
  })),
111
127
  {
112
- path: path.join(outputDir, "src", "index.ts"),
128
+ path: path.join(sourceDir, "index.ts"),
113
129
  content: generateIndex(dataModel)
114
- },
115
- // Package configuration files
130
+ }
131
+ ]
132
+
133
+ // Package configuration files (only when not filesOnly)
134
+ const packageFiles: Array<GeneratedFile> = filesOnly ? [] : [
116
135
  {
117
136
  path: path.join(outputDir, "package.json"),
118
137
  content: generatePackageJson(dataModel, packageConfig)
@@ -139,11 +158,13 @@ export const generate = (
139
158
  }
140
159
  ]
141
160
 
142
- // Create output directories
143
- yield* fs.makeDirectory(path.join(outputDir, "src"), { recursive: true }).pipe(
161
+ const files = [...sourceFiles, ...packageFiles]
162
+
163
+ // Create output directory
164
+ yield* fs.makeDirectory(sourceDir, { recursive: true }).pipe(
144
165
  Effect.mapError((error) =>
145
166
  new GeneratorError({
146
- message: `Failed to create output directory: ${outputDir}/src`,
167
+ message: `Failed to create output directory: ${sourceDir}`,
147
168
  cause: error
148
169
  })
149
170
  )
@@ -12,7 +12,8 @@ import {
12
12
  getQueryInterfaceName,
13
13
  getServiceClassName
14
14
  } from "./NamingHelper.js"
15
- import { getPromiseServiceName } from "./ServiceFnPromiseGenerator.js"
15
+ import { getPathBuildersModuleName } from "./NavigationGenerator.js"
16
+ import { getOperationsModuleName } from "./OperationsGenerator.js"
16
17
 
17
18
  /**
18
19
  * Generate the index.ts file content.
@@ -63,93 +64,41 @@ export const generateIndex = (dataModel: DataModel): string => {
63
64
  lines.push(` ${modelExports[i]}${isLast ? "" : ","}`)
64
65
  }
65
66
 
66
- lines.push(`} from "./Models.js"`)
67
+ lines.push(`} from "./Models"`)
67
68
  lines.push(``)
68
69
 
69
- // Re-export OData infrastructure
70
- const isV4 = dataModel.version === "V4"
71
- lines.push(`// Re-export OData infrastructure from @odata-effect/odata-effect`)
70
+ // Entity Services (all in one file using crud factory)
71
+ lines.push(`// Entity Services`)
72
+ lines.push(`// Use toPromise(runtime) from PathBuilders to convert Effect to Promise`)
72
73
  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,`)
74
+ const serviceExports: Array<string> = []
75
+ for (const entitySet of dataModel.entitySets.values()) {
76
+ serviceExports.push(getServiceClassName(entitySet.name))
83
77
  }
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,`)
78
+ // Also export types
79
+ serviceExports.push(`type CrudError`)
80
+ serviceExports.push(`type CrudContext`)
81
+ serviceExports.push(`type CrudService`)
82
+ for (let i = 0; i < serviceExports.length; i++) {
83
+ const isLast = i === serviceExports.length - 1
84
+ lines.push(` ${serviceExports[i]}${isLast ? "" : ","}`)
119
85
  }
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"`)
86
+ lines.push(`} from "./Services"`)
137
87
  lines.push(``)
138
88
 
139
- // Individual Entity Services (tree-shakable module namespace re-exports)
140
- lines.push(`// Individual Entity Services (tree-shakable)`)
141
- for (const entitySet of dataModel.entitySets.values()) {
142
- const serviceClassName = getServiceClassName(entitySet.name)
143
- lines.push(`export * as ${serviceClassName} from "./${serviceClassName}.js"`)
89
+ // Operations (FunctionImports, Functions, Actions) - only if there are unbound operations
90
+ const hasUnboundOperations = Array.from(dataModel.operations.values()).some((op) => !op.isBound)
91
+ if (hasUnboundOperations) {
92
+ lines.push(`// Operations (FunctionImports, Functions, Actions)`)
93
+ const operationsModuleName = getOperationsModuleName()
94
+ lines.push(`export * as ${operationsModuleName} from "./${operationsModuleName}"`)
95
+ lines.push(``)
144
96
  }
145
- lines.push(``)
146
97
 
147
- // Promise-based Entity Services (for non-Effect environments)
148
- lines.push(`// Promise-based Entity Services (for non-Effect environments)`)
149
- for (const entitySet of dataModel.entitySets.values()) {
150
- const promiseServiceName = getPromiseServiceName(entitySet.name)
151
- lines.push(`export * as ${promiseServiceName} from "./${promiseServiceName}.js"`)
152
- }
98
+ // Path Builders (tree-shakable navigation)
99
+ lines.push(`// Path Builders (tree-shakable navigation)`)
100
+ const pathBuildersModuleName = getPathBuildersModuleName()
101
+ lines.push(`export * from "./${pathBuildersModuleName}"`)
153
102
  lines.push(``)
154
103
 
155
104
  // Query Models
@@ -183,7 +132,7 @@ export const generateIndex = (dataModel: DataModel): string => {
183
132
  lines.push(` ${queryExports[i]}${isLast ? "" : ","}`)
184
133
  }
185
134
 
186
- lines.push(`} from "./QueryModels.js"`)
135
+ lines.push(`} from "./QueryModels"`)
187
136
 
188
137
  return lines.join("\n")
189
138
  }
@@ -243,15 +243,12 @@ const generateEntityType = (
243
243
  lines.push(` Schema.Struct({ ${key.name}: ${keySchema} })`)
244
244
  lines.push(`)`)
245
245
  } else {
246
- // Composite key
246
+ // Composite key - only struct form makes sense
247
247
  const keyFields = entityType.keys.map((k) => {
248
248
  const schema = getPropertySchemaType(k, false)
249
249
  return `${k.name}: ${schema}`
250
250
  })
251
- lines.push(`export const ${idTypeName} = Schema.Union(`)
252
- lines.push(` Schema.String,`)
253
- lines.push(` Schema.Struct({ ${keyFields.join(", ")} })`)
254
- lines.push(`)`)
251
+ lines.push(`export const ${idTypeName} = Schema.Struct({ ${keyFields.join(", ")} })`)
255
252
  }
256
253
  lines.push(`export type ${idTypeName} = Schema.Schema.Type<typeof ${idTypeName}>`)
257
254
  }
@@ -84,10 +84,17 @@ 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 (
90
+ singular.endsWith("xes") || singular.endsWith("ches") ||
91
+ singular.endsWith("shes") || singular.endsWith("sses") ||
92
+ singular.endsWith("zes")
93
+ ) {
94
+ // "Boxes" -> "Box", "Beaches" -> "Beach", etc.
89
95
  singular = singular.slice(0, -2)
90
96
  } else if (singular.endsWith("s") && !singular.endsWith("ss")) {
97
+ // "Products" -> "Product", "Airlines" -> "Airline"
91
98
  singular = singular.slice(0, -1)
92
99
  }
93
100
  return `${toPascalCase(singular)}Service`