@odata-effect/odata-effect-generator 0.1.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 (131) hide show
  1. package/Cli/package.json +6 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -0
  4. package/bin/package.json +6 -0
  5. package/digester/Digester/package.json +6 -0
  6. package/digester/TypeMapper/package.json +6 -0
  7. package/dist/cjs/Cli.js +95 -0
  8. package/dist/cjs/Cli.js.map +1 -0
  9. package/dist/cjs/bin.js +14 -0
  10. package/dist/cjs/bin.js.map +1 -0
  11. package/dist/cjs/digester/Digester.js +488 -0
  12. package/dist/cjs/digester/Digester.js.map +1 -0
  13. package/dist/cjs/digester/TypeMapper.js +262 -0
  14. package/dist/cjs/digester/TypeMapper.js.map +1 -0
  15. package/dist/cjs/generator/Generator.js +126 -0
  16. package/dist/cjs/generator/Generator.js.map +1 -0
  17. package/dist/cjs/generator/IndexGenerator.js +164 -0
  18. package/dist/cjs/generator/IndexGenerator.js.map +1 -0
  19. package/dist/cjs/generator/ModelsGenerator.js +255 -0
  20. package/dist/cjs/generator/ModelsGenerator.js.map +1 -0
  21. package/dist/cjs/generator/NamingHelper.js +164 -0
  22. package/dist/cjs/generator/NamingHelper.js.map +1 -0
  23. package/dist/cjs/generator/PackageGenerator.js +168 -0
  24. package/dist/cjs/generator/PackageGenerator.js.map +1 -0
  25. package/dist/cjs/generator/QueryModelsGenerator.js +182 -0
  26. package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -0
  27. package/dist/cjs/generator/ServiceFnGenerator.js +258 -0
  28. package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -0
  29. package/dist/cjs/generator/ServiceFnPromiseGenerator.js +182 -0
  30. package/dist/cjs/generator/ServiceFnPromiseGenerator.js.map +1 -0
  31. package/dist/cjs/index.js +38 -0
  32. package/dist/cjs/index.js.map +1 -0
  33. package/dist/cjs/model/DataModel.js +28 -0
  34. package/dist/cjs/model/DataModel.js.map +1 -0
  35. package/dist/cjs/parser/EdmxSchema.js +20 -0
  36. package/dist/cjs/parser/EdmxSchema.js.map +1 -0
  37. package/dist/cjs/parser/XmlParser.js +47 -0
  38. package/dist/cjs/parser/XmlParser.js.map +1 -0
  39. package/dist/dts/Cli.d.ts +9 -0
  40. package/dist/dts/Cli.d.ts.map +1 -0
  41. package/dist/dts/bin.d.ts +3 -0
  42. package/dist/dts/bin.d.ts.map +1 -0
  43. package/dist/dts/digester/Digester.d.ts +33 -0
  44. package/dist/dts/digester/Digester.d.ts.map +1 -0
  45. package/dist/dts/digester/TypeMapper.d.ts +79 -0
  46. package/dist/dts/digester/TypeMapper.d.ts.map +1 -0
  47. package/dist/dts/generator/Generator.d.ts +45 -0
  48. package/dist/dts/generator/Generator.d.ts.map +1 -0
  49. package/dist/dts/generator/IndexGenerator.d.ts +14 -0
  50. package/dist/dts/generator/IndexGenerator.d.ts.map +1 -0
  51. package/dist/dts/generator/ModelsGenerator.d.ts +14 -0
  52. package/dist/dts/generator/ModelsGenerator.d.ts.map +1 -0
  53. package/dist/dts/generator/NamingHelper.d.ts +110 -0
  54. package/dist/dts/generator/NamingHelper.d.ts.map +1 -0
  55. package/dist/dts/generator/PackageGenerator.d.ts +53 -0
  56. package/dist/dts/generator/PackageGenerator.d.ts.map +1 -0
  57. package/dist/dts/generator/QueryModelsGenerator.d.ts +14 -0
  58. package/dist/dts/generator/QueryModelsGenerator.d.ts.map +1 -0
  59. package/dist/dts/generator/ServiceFnGenerator.d.ts +37 -0
  60. package/dist/dts/generator/ServiceFnGenerator.d.ts.map +1 -0
  61. package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts +40 -0
  62. package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts.map +1 -0
  63. package/dist/dts/index.d.ts +101 -0
  64. package/dist/dts/index.d.ts.map +1 -0
  65. package/dist/dts/model/DataModel.d.ts +188 -0
  66. package/dist/dts/model/DataModel.d.ts.map +1 -0
  67. package/dist/dts/parser/EdmxSchema.d.ts +228 -0
  68. package/dist/dts/parser/EdmxSchema.d.ts.map +1 -0
  69. package/dist/dts/parser/XmlParser.d.ts +31 -0
  70. package/dist/dts/parser/XmlParser.d.ts.map +1 -0
  71. package/dist/esm/Cli.js +87 -0
  72. package/dist/esm/Cli.js.map +1 -0
  73. package/dist/esm/bin.js +12 -0
  74. package/dist/esm/bin.js.map +1 -0
  75. package/dist/esm/digester/Digester.js +478 -0
  76. package/dist/esm/digester/Digester.js.map +1 -0
  77. package/dist/esm/digester/TypeMapper.js +248 -0
  78. package/dist/esm/digester/TypeMapper.js.map +1 -0
  79. package/dist/esm/generator/Generator.js +116 -0
  80. package/dist/esm/generator/Generator.js.map +1 -0
  81. package/dist/esm/generator/IndexGenerator.js +157 -0
  82. package/dist/esm/generator/IndexGenerator.js.map +1 -0
  83. package/dist/esm/generator/ModelsGenerator.js +248 -0
  84. package/dist/esm/generator/ModelsGenerator.js.map +1 -0
  85. package/dist/esm/generator/NamingHelper.js +147 -0
  86. package/dist/esm/generator/NamingHelper.js.map +1 -0
  87. package/dist/esm/generator/PackageGenerator.js +156 -0
  88. package/dist/esm/generator/PackageGenerator.js.map +1 -0
  89. package/dist/esm/generator/QueryModelsGenerator.js +175 -0
  90. package/dist/esm/generator/QueryModelsGenerator.js.map +1 -0
  91. package/dist/esm/generator/ServiceFnGenerator.js +251 -0
  92. package/dist/esm/generator/ServiceFnGenerator.js.map +1 -0
  93. package/dist/esm/generator/ServiceFnPromiseGenerator.js +174 -0
  94. package/dist/esm/generator/ServiceFnPromiseGenerator.js.map +1 -0
  95. package/dist/esm/index.js +101 -0
  96. package/dist/esm/index.js.map +1 -0
  97. package/dist/esm/model/DataModel.js +21 -0
  98. package/dist/esm/model/DataModel.js.map +1 -0
  99. package/dist/esm/package.json +4 -0
  100. package/dist/esm/parser/EdmxSchema.js +13 -0
  101. package/dist/esm/parser/EdmxSchema.js.map +1 -0
  102. package/dist/esm/parser/XmlParser.js +37 -0
  103. package/dist/esm/parser/XmlParser.js.map +1 -0
  104. package/generator/Generator/package.json +6 -0
  105. package/generator/IndexGenerator/package.json +6 -0
  106. package/generator/ModelsGenerator/package.json +6 -0
  107. package/generator/NamingHelper/package.json +6 -0
  108. package/generator/PackageGenerator/package.json +6 -0
  109. package/generator/QueryModelsGenerator/package.json +6 -0
  110. package/generator/ServiceFnGenerator/package.json +6 -0
  111. package/generator/ServiceFnPromiseGenerator/package.json +6 -0
  112. package/model/DataModel/package.json +6 -0
  113. package/package.json +157 -0
  114. package/parser/EdmxSchema/package.json +6 -0
  115. package/parser/XmlParser/package.json +6 -0
  116. package/src/Cli.ts +115 -0
  117. package/src/bin.ts +17 -0
  118. package/src/digester/Digester.ts +600 -0
  119. package/src/digester/TypeMapper.ts +181 -0
  120. package/src/generator/Generator.ts +183 -0
  121. package/src/generator/IndexGenerator.ts +189 -0
  122. package/src/generator/ModelsGenerator.ts +344 -0
  123. package/src/generator/NamingHelper.ts +159 -0
  124. package/src/generator/PackageGenerator.ts +176 -0
  125. package/src/generator/QueryModelsGenerator.ts +228 -0
  126. package/src/generator/ServiceFnGenerator.ts +324 -0
  127. package/src/generator/ServiceFnPromiseGenerator.ts +242 -0
  128. package/src/index.ts +114 -0
  129. package/src/model/DataModel.ts +254 -0
  130. package/src/parser/EdmxSchema.ts +308 -0
  131. package/src/parser/XmlParser.ts +47 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Generator for QueryModels.ts - Type-safe query paths.
3
+ *
4
+ * @since 1.0.0
5
+ */
6
+ import type {
7
+ ComplexTypeModel,
8
+ DataModel,
9
+ EntityTypeModel,
10
+ NavigationPropertyModel,
11
+ PropertyModel
12
+ } from "../model/DataModel.js"
13
+ import { getClassName, getQueryFactoryName, getQueryInstanceName, getQueryInterfaceName } from "./NamingHelper.js"
14
+
15
+ /**
16
+ * Generate the QueryModels.ts file content.
17
+ *
18
+ * @since 1.0.0
19
+ * @category generation
20
+ */
21
+ export const generateQueryModels = (dataModel: DataModel): string => {
22
+ const lines: Array<string> = []
23
+
24
+ // Collect all query path types needed
25
+ const queryPathTypes = collectQueryPathTypes(dataModel)
26
+
27
+ // Header
28
+ lines.push(`/**`)
29
+ lines.push(` * Type-safe query paths for ${dataModel.serviceName} entities.`)
30
+ lines.push(` * Generated by odata-effect-gen.`)
31
+ lines.push(` *`)
32
+ lines.push(` * @since 1.0.0`)
33
+ lines.push(` */`)
34
+ lines.push(`import {`)
35
+ lines.push(` ${Array.from(queryPathTypes).join(",\n ")},`)
36
+ lines.push(` createQueryBuilder,`)
37
+ lines.push(` type QueryBuilder`)
38
+ lines.push(`} from "@odata-effect/odata-effect"`)
39
+
40
+ // Import entity types that have entity sets (for QueryBuilder generics)
41
+ const usedEntityNames: Array<string> = []
42
+ for (const entitySet of dataModel.entitySets.values()) {
43
+ const entityType = dataModel.entityTypes.get(entitySet.entityTypeFqName)
44
+ if (entityType) {
45
+ usedEntityNames.push(entityType.name)
46
+ }
47
+ }
48
+ if (usedEntityNames.length > 0) {
49
+ lines.push(`import type {`)
50
+ lines.push(` ${usedEntityNames.join(",\n ")}`)
51
+ lines.push(`} from "./Models.js"`)
52
+ }
53
+ lines.push(``)
54
+
55
+ // Generate complex type query paths
56
+ for (const complexType of dataModel.complexTypes.values()) {
57
+ for (const line of generateQueryInterface(complexType)) lines.push(line)
58
+ lines.push(``)
59
+ for (const line of generateQueryInstance(complexType)) lines.push(line)
60
+ lines.push(``)
61
+ }
62
+
63
+ // Generate entity type query paths
64
+ for (const entityType of dataModel.entityTypes.values()) {
65
+ for (const line of generateQueryInterface(entityType)) lines.push(line)
66
+ lines.push(``)
67
+ for (const line of generateQueryInstance(entityType)) lines.push(line)
68
+ lines.push(``)
69
+ }
70
+
71
+ // Generate query builder factories for entity sets
72
+ for (const entitySet of dataModel.entitySets.values()) {
73
+ const entityType = dataModel.entityTypes.get(entitySet.entityTypeFqName)
74
+ if (entityType) {
75
+ for (const line of generateQueryFactory(entityType)) lines.push(line)
76
+ lines.push(``)
77
+ }
78
+ }
79
+
80
+ return lines.join("\n")
81
+ }
82
+
83
+ /**
84
+ * Collect all query path types used in the model.
85
+ */
86
+ const collectQueryPathTypes = (dataModel: DataModel): Set<string> => {
87
+ const types = new Set<string>()
88
+
89
+ const addPropertyTypes = (props: ReadonlyArray<PropertyModel>) => {
90
+ for (const prop of props) {
91
+ types.add(prop.typeMapping.queryPath.split("<")[0])
92
+ }
93
+ }
94
+
95
+ const addNavPropertyTypes = (navProps: ReadonlyArray<NavigationPropertyModel>) => {
96
+ for (const navProp of navProps) {
97
+ types.add(navProp.isCollection ? "CollectionPath" : "EntityPath")
98
+ }
99
+ }
100
+
101
+ for (const complexType of dataModel.complexTypes.values()) {
102
+ addPropertyTypes(complexType.properties)
103
+ addNavPropertyTypes(complexType.navigationProperties)
104
+ }
105
+
106
+ for (const entityType of dataModel.entityTypes.values()) {
107
+ addPropertyTypes(entityType.properties)
108
+ addNavPropertyTypes(entityType.navigationProperties)
109
+ }
110
+
111
+ return types
112
+ }
113
+
114
+ /**
115
+ * Generate a query interface for a type.
116
+ */
117
+ const generateQueryInterface = (
118
+ type: EntityTypeModel | ComplexTypeModel
119
+ ): Array<string> => {
120
+ const lines: Array<string> = []
121
+ const interfaceName = getQueryInterfaceName(type.name)
122
+
123
+ lines.push(`/**`)
124
+ lines.push(` * Query paths for ${type.odataName}.`)
125
+ lines.push(` *`)
126
+ lines.push(` * @since 1.0.0`)
127
+ lines.push(` * @category query-paths`)
128
+ lines.push(` */`)
129
+ lines.push(`export interface ${interfaceName} {`)
130
+
131
+ for (const prop of type.properties) {
132
+ const basePathType = prop.typeMapping.queryPath
133
+ // If it's a collection of complex/entity types, wrap in CollectionPath
134
+ const pathType = prop.isCollection && basePathType.startsWith("EntityPath<")
135
+ ? basePathType.replace("EntityPath<", "CollectionPath<")
136
+ : basePathType
137
+ lines.push(` readonly ${prop.name}: ${pathType}`)
138
+ }
139
+
140
+ for (const navProp of type.navigationProperties) {
141
+ const targetQName = getQueryInterfaceName(getClassName(navProp.targetType))
142
+ const pathType = navProp.isCollection
143
+ ? `CollectionPath<${targetQName}>`
144
+ : `EntityPath<${targetQName}>`
145
+ lines.push(` readonly ${navProp.name}: ${pathType}`)
146
+ }
147
+
148
+ lines.push(`}`)
149
+
150
+ return lines
151
+ }
152
+
153
+ /**
154
+ * Generate a query instance for a type.
155
+ */
156
+ const generateQueryInstance = (
157
+ type: EntityTypeModel | ComplexTypeModel
158
+ ): Array<string> => {
159
+ const lines: Array<string> = []
160
+ const interfaceName = getQueryInterfaceName(type.name)
161
+ const instanceName = getQueryInstanceName(type.name)
162
+
163
+ lines.push(`/**`)
164
+ lines.push(` * Query paths instance for ${type.odataName}.`)
165
+ lines.push(` *`)
166
+ lines.push(` * @since 1.0.0`)
167
+ lines.push(` * @category query-paths`)
168
+ lines.push(` */`)
169
+ lines.push(`export const ${instanceName}: ${interfaceName} = {`)
170
+
171
+ const allFields = [...type.properties, ...type.navigationProperties]
172
+ for (let i = 0; i < type.properties.length; i++) {
173
+ const prop = type.properties[i]
174
+ let pathClass = prop.typeMapping.queryPath.split("<")[0]
175
+ // If it's a collection of complex/entity types, use CollectionPath instead of EntityPath
176
+ if (prop.isCollection && pathClass === "EntityPath") {
177
+ pathClass = "CollectionPath"
178
+ }
179
+ const isLast = i === allFields.length - 1
180
+
181
+ // For EntityPath and CollectionPath, we need to provide the nested query paths
182
+ if (pathClass === "EntityPath" || pathClass === "CollectionPath") {
183
+ // Extract the target type from the queryPath generic (e.g., "EntityPath<QAddress>" -> "QAddress")
184
+ const targetMatch = prop.typeMapping.queryPath.match(/<Q(\w+)>/)
185
+ const targetInstanceName = targetMatch ? `q${targetMatch[1]}` : "undefined"
186
+ lines.push(
187
+ ` ${prop.name}: new ${pathClass}("${prop.odataName}", () => ${targetInstanceName})${isLast ? "" : ","}`
188
+ )
189
+ } else {
190
+ lines.push(` ${prop.name}: new ${pathClass}("${prop.odataName}")${isLast ? "" : ","}`)
191
+ }
192
+ }
193
+
194
+ for (let i = 0; i < type.navigationProperties.length; i++) {
195
+ const navProp = type.navigationProperties[i]
196
+ const targetInstanceName = getQueryInstanceName(getClassName(navProp.targetType))
197
+ const pathClass = navProp.isCollection ? "CollectionPath" : "EntityPath"
198
+ const isLast = i === type.navigationProperties.length - 1
199
+ lines.push(
200
+ ` ${navProp.name}: new ${pathClass}("${navProp.odataName}", () => ${targetInstanceName})${isLast ? "" : ","}`
201
+ )
202
+ }
203
+
204
+ lines.push(`}`)
205
+
206
+ return lines
207
+ }
208
+
209
+ /**
210
+ * Generate a query builder factory for an entity type.
211
+ */
212
+ const generateQueryFactory = (entityType: EntityTypeModel): Array<string> => {
213
+ const lines: Array<string> = []
214
+ const interfaceName = getQueryInterfaceName(entityType.name)
215
+ const instanceName = getQueryInstanceName(entityType.name)
216
+ const factoryName = getQueryFactoryName(entityType.name)
217
+
218
+ lines.push(`/**`)
219
+ lines.push(` * Creates a type-safe query builder for ${entityType.odataName} entities.`)
220
+ lines.push(` *`)
221
+ lines.push(` * @since 1.0.0`)
222
+ lines.push(` * @category constructors`)
223
+ lines.push(` */`)
224
+ lines.push(`export const ${factoryName} = (): QueryBuilder<${entityType.name}, ${interfaceName}> =>`)
225
+ lines.push(` createQueryBuilder<${entityType.name}, ${interfaceName}>(${instanceName})`)
226
+
227
+ return lines
228
+ }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Generator for tree-shakable service functions.
3
+ *
4
+ * This module generates standalone functions that can be tree-shaken.
5
+ * Each entity service is a module of standalone functions that use
6
+ * the tree-shakable OData functions directly.
7
+ *
8
+ * @since 1.0.0
9
+ */
10
+ import type { DataModel, EntitySetModel, EntityTypeModel } from "../model/DataModel.js"
11
+ import type { ODataVersion } from "../parser/EdmxSchema.js"
12
+ import { getEditableTypeName, getIdTypeName, getServiceClassName } from "./NamingHelper.js"
13
+
14
+ /**
15
+ * Version-specific imports and identifiers.
16
+ */
17
+ interface VersionConfig {
18
+ readonly odataNamespace: string
19
+ readonly buildEntityPath: string
20
+ readonly queryOptionsType: string
21
+ readonly clientConfigTag: string
22
+ }
23
+
24
+ const V2_CONFIG: VersionConfig = {
25
+ odataNamespace: "OData",
26
+ buildEntityPath: "buildEntityPath",
27
+ queryOptionsType: "ODataQueryOptions",
28
+ clientConfigTag: "ODataClientConfig"
29
+ }
30
+
31
+ const V4_CONFIG: VersionConfig = {
32
+ odataNamespace: "ODataV4",
33
+ buildEntityPath: "buildEntityPathV4",
34
+ queryOptionsType: "ODataV4QueryOptions",
35
+ clientConfigTag: "ODataV4ClientConfig"
36
+ }
37
+
38
+ const getVersionConfig = (version: ODataVersion): VersionConfig => version === "V4" ? V4_CONFIG : V2_CONFIG
39
+
40
+ /**
41
+ * Generated service function file.
42
+ *
43
+ * @since 1.0.0
44
+ * @category types
45
+ */
46
+ export interface GeneratedServiceFnFile {
47
+ readonly fileName: string
48
+ readonly content: string
49
+ }
50
+
51
+ /**
52
+ * Result of service function generation.
53
+ *
54
+ * @since 1.0.0
55
+ * @category types
56
+ */
57
+ export interface ServiceFnGenerationResult {
58
+ readonly entityServices: ReadonlyArray<GeneratedServiceFnFile>
59
+ }
60
+
61
+ /**
62
+ * Generate all tree-shakable service function files.
63
+ *
64
+ * @since 1.0.0
65
+ * @category generation
66
+ */
67
+ export const generateServiceFns = (dataModel: DataModel): ServiceFnGenerationResult => {
68
+ const entityServices: Array<GeneratedServiceFnFile> = []
69
+
70
+ // Generate individual entity service files
71
+ for (const entitySet of dataModel.entitySets.values()) {
72
+ const entityType = dataModel.entityTypes.get(entitySet.entityTypeFqName)
73
+ if (entityType) {
74
+ const serviceClassName = getServiceClassName(entitySet.name)
75
+ entityServices.push({
76
+ fileName: `${serviceClassName}.ts`,
77
+ content: generateEntityServiceFnFile(entitySet, entityType, dataModel)
78
+ })
79
+ }
80
+ }
81
+
82
+ return { entityServices }
83
+ }
84
+
85
+ /**
86
+ * Generate a single entity service function file.
87
+ */
88
+ const generateEntityServiceFnFile = (
89
+ entitySet: EntitySetModel,
90
+ entityType: EntityTypeModel,
91
+ dataModel: DataModel
92
+ ): string => {
93
+ const lines: Array<string> = []
94
+ const versionConfig = getVersionConfig(dataModel.version)
95
+ const serviceClassName = getServiceClassName(entitySet.name)
96
+ const entityName = entityType.name
97
+ const idTypeName = getIdTypeName(entityName)
98
+ const editableName = getEditableTypeName(entityName)
99
+ const hasKeys = entityType.keys.length > 0
100
+ const isV4 = dataModel.version === "V4"
101
+
102
+ // Header and imports
103
+ lines.push(`/**`)
104
+ lines.push(
105
+ ` * Tree-shakable ${entityName} service functions for ${dataModel.serviceName} OData ${dataModel.version}.`
106
+ )
107
+ lines.push(` * Generated by odata-effect-gen.`)
108
+ lines.push(` *`)
109
+ lines.push(` * @example`)
110
+ lines.push(` * \`\`\`ts`)
111
+ lines.push(` * // Namespace import - nice autocomplete, tree-shakable`)
112
+ lines.push(` * import { ${serviceClassName} } from "@template/${dataModel.serviceName.toLowerCase()}-effect"`)
113
+ lines.push(` * const items = yield* ${serviceClassName}.getAll()`)
114
+ lines.push(` *`)
115
+ lines.push(` * // Direct import - maximum tree-shaking`)
116
+ lines.push(` * import { getAll } from "@template/${dataModel.serviceName.toLowerCase()}-effect/${serviceClassName}"`)
117
+ lines.push(` * const items = yield* getAll()`)
118
+ lines.push(` * \`\`\``)
119
+ lines.push(` *`)
120
+ lines.push(` * @since 1.0.0`)
121
+ lines.push(` */`)
122
+ lines.push(`import type * as Effect from "effect/Effect"`)
123
+ lines.push(`import * as Schema from "effect/Schema"`)
124
+ lines.push(`import type { HttpClient } from "@effect/platform"`)
125
+ lines.push(`import type * as HttpClientError from "@effect/platform/HttpClientError"`)
126
+ lines.push(`import type * as HttpBody from "@effect/platform/HttpBody"`)
127
+ lines.push(``)
128
+
129
+ // Import models
130
+ lines.push(`import {`)
131
+ lines.push(` ${entityName},`)
132
+ if (hasKeys) {
133
+ lines.push(` type ${idTypeName},`)
134
+ }
135
+ lines.push(` ${editableName}`)
136
+ lines.push(`} from "./Models.js"`)
137
+ lines.push(``)
138
+
139
+ // Import OData infrastructure (version-specific)
140
+ lines.push(`import {`)
141
+ lines.push(` ${versionConfig.odataNamespace},`)
142
+ lines.push(` ${versionConfig.buildEntityPath},`)
143
+ lines.push(` ${versionConfig.clientConfigTag},`)
144
+ lines.push(` type ${versionConfig.queryOptionsType},`)
145
+ if (!isV4) {
146
+ lines.push(` type SapError,`)
147
+ }
148
+ lines.push(` type ODataError,`)
149
+ lines.push(` type ParseError`)
150
+ lines.push(`} from "@odata-effect/odata-effect"`)
151
+ lines.push(``)
152
+
153
+ // Service error type
154
+ lines.push(`// ============================================================================`)
155
+ lines.push(`// Types`)
156
+ lines.push(`// ============================================================================`)
157
+ lines.push(``)
158
+ lines.push(`/**`)
159
+ lines.push(` * Error type for ${entityName} service operations.`)
160
+ lines.push(` *`)
161
+ lines.push(` * @since 1.0.0`)
162
+ lines.push(` * @category errors`)
163
+ lines.push(` */`)
164
+ lines.push(`export type ${serviceClassName}Error =`)
165
+ lines.push(` | HttpClientError.HttpClientError`)
166
+ lines.push(` | HttpBody.HttpBodyError`)
167
+ lines.push(` | ParseError`)
168
+ if (!isV4) {
169
+ lines.push(` | SapError`)
170
+ }
171
+ lines.push(` | ODataError`)
172
+ lines.push(``)
173
+
174
+ // Dependencies type
175
+ lines.push(`/**`)
176
+ lines.push(` * Dependencies required for ${entityName} service operations.`)
177
+ lines.push(` *`)
178
+ lines.push(` * @since 1.0.0`)
179
+ lines.push(` * @category context`)
180
+ lines.push(` */`)
181
+ lines.push(`export type ${serviceClassName}Context = ${versionConfig.clientConfigTag} | HttpClient.HttpClient`)
182
+ lines.push(``)
183
+
184
+ // ID normalizer (if entity has keys)
185
+ if (hasKeys) {
186
+ lines.push(`// ============================================================================`)
187
+ lines.push(`// Internal Helpers`)
188
+ lines.push(`// ============================================================================`)
189
+ lines.push(``)
190
+ const normalizeIdFn = `normalizeId`
191
+ lines.push(`const ${normalizeIdFn} = (id: ${idTypeName}): string =>`)
192
+
193
+ if (entityType.keys.length === 1) {
194
+ const keyProp = entityType.keys[0]
195
+ const keyTsType = keyProp.typeMapping.tsType
196
+ const primitiveCheck = keyTsType === "number" ? "number" : "string"
197
+ lines.push(` typeof id === "${primitiveCheck}" ? String(id) : String(id.${keyProp.name})`)
198
+ } else {
199
+ const keyParts = entityType.keys.map((k) => `${k.odataName}='\${String(id.${k.name})}'`).join(",")
200
+ lines.push(` typeof id === "string" ? id : \`${keyParts}\``)
201
+ }
202
+ lines.push(``)
203
+ }
204
+
205
+ // Standalone functions
206
+ lines.push(`// ============================================================================`)
207
+ lines.push(`// Standalone Tree-Shakable Functions`)
208
+ lines.push(`// ============================================================================`)
209
+ lines.push(``)
210
+
211
+ // getAll
212
+ lines.push(`/**`)
213
+ lines.push(` * Fetch all ${entityName} entities.`)
214
+ lines.push(` *`)
215
+ lines.push(` * @since 1.0.0`)
216
+ lines.push(` * @category operations`)
217
+ lines.push(` */`)
218
+ lines.push(`export const getAll = (`)
219
+ lines.push(` options?: ${versionConfig.queryOptionsType}`)
220
+ lines.push(`): Effect.Effect<`)
221
+ lines.push(` ReadonlyArray<${entityName}>,`)
222
+ lines.push(` ${serviceClassName}Error,`)
223
+ lines.push(` ${serviceClassName}Context`)
224
+ lines.push(`> =>`)
225
+ lines.push(` ${versionConfig.odataNamespace}.getCollection("${entitySet.name}", ${entityName}, options)`)
226
+ lines.push(``)
227
+
228
+ // getById (if entity has keys)
229
+ if (hasKeys) {
230
+ const keyObj = entityType.keys.length === 1
231
+ ? `{ ${entityType.keys[0].odataName}: normalizeId(id) }`
232
+ : `normalizeId(id)`
233
+
234
+ lines.push(`/**`)
235
+ lines.push(` * Fetch a single ${entityName} by ID.`)
236
+ lines.push(` *`)
237
+ lines.push(` * @since 1.0.0`)
238
+ lines.push(` * @category operations`)
239
+ lines.push(` */`)
240
+ lines.push(`export const getById = (`)
241
+ lines.push(` id: ${idTypeName}`)
242
+ lines.push(`): Effect.Effect<`)
243
+ lines.push(` ${entityName},`)
244
+ lines.push(` ${serviceClassName}Error,`)
245
+ lines.push(` ${serviceClassName}Context`)
246
+ lines.push(`> =>`)
247
+ lines.push(` ${versionConfig.odataNamespace}.get(`)
248
+ lines.push(` ${versionConfig.buildEntityPath}("${entitySet.name}", ${keyObj}),`)
249
+ lines.push(` ${entityName}`)
250
+ lines.push(` )`)
251
+ lines.push(``)
252
+ }
253
+
254
+ // create
255
+ lines.push(`/**`)
256
+ lines.push(` * Create a new ${entityName} entity.`)
257
+ lines.push(` *`)
258
+ lines.push(` * @since 1.0.0`)
259
+ lines.push(` * @category operations`)
260
+ lines.push(` */`)
261
+ lines.push(`export const create = (`)
262
+ lines.push(` entity: ${editableName}`)
263
+ lines.push(`): Effect.Effect<`)
264
+ lines.push(` ${entityName},`)
265
+ lines.push(` ${serviceClassName}Error,`)
266
+ lines.push(` ${serviceClassName}Context`)
267
+ lines.push(`> =>`)
268
+ lines.push(` ${versionConfig.odataNamespace}.post("${entitySet.name}", entity, ${editableName}, ${entityName})`)
269
+ lines.push(``)
270
+
271
+ // update (if entity has keys)
272
+ if (hasKeys) {
273
+ const keyObj = entityType.keys.length === 1
274
+ ? `{ ${entityType.keys[0].odataName}: normalizeId(id) }`
275
+ : `normalizeId(id)`
276
+
277
+ lines.push(`/**`)
278
+ lines.push(` * Update an existing ${entityName} entity.`)
279
+ lines.push(` *`)
280
+ lines.push(` * @since 1.0.0`)
281
+ lines.push(` * @category operations`)
282
+ lines.push(` */`)
283
+ lines.push(`export const update = (`)
284
+ lines.push(` id: ${idTypeName},`)
285
+ lines.push(` entity: Partial<${editableName}>`)
286
+ lines.push(`): Effect.Effect<`)
287
+ lines.push(` void,`)
288
+ lines.push(` ${serviceClassName}Error,`)
289
+ lines.push(` ${serviceClassName}Context`)
290
+ lines.push(`> =>`)
291
+ lines.push(` ${versionConfig.odataNamespace}.patch(`)
292
+ lines.push(` ${versionConfig.buildEntityPath}("${entitySet.name}", ${keyObj}),`)
293
+ lines.push(` entity,`)
294
+ lines.push(` Schema.partial(${editableName})`)
295
+ lines.push(` )`)
296
+ lines.push(``)
297
+
298
+ // delete
299
+ lines.push(`/**`)
300
+ lines.push(` * Delete a ${entityName} entity.`)
301
+ lines.push(` *`)
302
+ lines.push(` * @since 1.0.0`)
303
+ lines.push(` * @category operations`)
304
+ lines.push(` */`)
305
+ lines.push(`export const del = (`)
306
+ lines.push(` id: ${idTypeName}`)
307
+ lines.push(`): Effect.Effect<`)
308
+ lines.push(` void,`)
309
+ lines.push(` ${serviceClassName}Error,`)
310
+ lines.push(` ${serviceClassName}Context`)
311
+ lines.push(`> =>`)
312
+ lines.push(` ${versionConfig.odataNamespace}.del(`)
313
+ lines.push(` ${versionConfig.buildEntityPath}("${entitySet.name}", ${keyObj})`)
314
+ lines.push(` )`)
315
+ lines.push(``)
316
+
317
+ // Alias for nice naming
318
+ lines.push(`// Alias for nice naming`)
319
+ lines.push(`export { del as delete }`)
320
+ lines.push(``)
321
+ }
322
+
323
+ return lines.join("\n")
324
+ }