@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.
- package/Cli/package.json +6 -0
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/bin/package.json +6 -0
- package/digester/Digester/package.json +6 -0
- package/digester/TypeMapper/package.json +6 -0
- package/dist/cjs/Cli.js +95 -0
- package/dist/cjs/Cli.js.map +1 -0
- package/dist/cjs/bin.js +14 -0
- package/dist/cjs/bin.js.map +1 -0
- package/dist/cjs/digester/Digester.js +488 -0
- package/dist/cjs/digester/Digester.js.map +1 -0
- package/dist/cjs/digester/TypeMapper.js +262 -0
- package/dist/cjs/digester/TypeMapper.js.map +1 -0
- package/dist/cjs/generator/Generator.js +126 -0
- package/dist/cjs/generator/Generator.js.map +1 -0
- package/dist/cjs/generator/IndexGenerator.js +164 -0
- package/dist/cjs/generator/IndexGenerator.js.map +1 -0
- package/dist/cjs/generator/ModelsGenerator.js +255 -0
- package/dist/cjs/generator/ModelsGenerator.js.map +1 -0
- package/dist/cjs/generator/NamingHelper.js +164 -0
- package/dist/cjs/generator/NamingHelper.js.map +1 -0
- package/dist/cjs/generator/PackageGenerator.js +168 -0
- package/dist/cjs/generator/PackageGenerator.js.map +1 -0
- package/dist/cjs/generator/QueryModelsGenerator.js +182 -0
- package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -0
- package/dist/cjs/generator/ServiceFnGenerator.js +258 -0
- package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -0
- package/dist/cjs/generator/ServiceFnPromiseGenerator.js +182 -0
- package/dist/cjs/generator/ServiceFnPromiseGenerator.js.map +1 -0
- package/dist/cjs/index.js +38 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/model/DataModel.js +28 -0
- package/dist/cjs/model/DataModel.js.map +1 -0
- package/dist/cjs/parser/EdmxSchema.js +20 -0
- package/dist/cjs/parser/EdmxSchema.js.map +1 -0
- package/dist/cjs/parser/XmlParser.js +47 -0
- package/dist/cjs/parser/XmlParser.js.map +1 -0
- package/dist/dts/Cli.d.ts +9 -0
- package/dist/dts/Cli.d.ts.map +1 -0
- package/dist/dts/bin.d.ts +3 -0
- package/dist/dts/bin.d.ts.map +1 -0
- package/dist/dts/digester/Digester.d.ts +33 -0
- package/dist/dts/digester/Digester.d.ts.map +1 -0
- package/dist/dts/digester/TypeMapper.d.ts +79 -0
- package/dist/dts/digester/TypeMapper.d.ts.map +1 -0
- package/dist/dts/generator/Generator.d.ts +45 -0
- package/dist/dts/generator/Generator.d.ts.map +1 -0
- package/dist/dts/generator/IndexGenerator.d.ts +14 -0
- package/dist/dts/generator/IndexGenerator.d.ts.map +1 -0
- package/dist/dts/generator/ModelsGenerator.d.ts +14 -0
- package/dist/dts/generator/ModelsGenerator.d.ts.map +1 -0
- package/dist/dts/generator/NamingHelper.d.ts +110 -0
- package/dist/dts/generator/NamingHelper.d.ts.map +1 -0
- package/dist/dts/generator/PackageGenerator.d.ts +53 -0
- package/dist/dts/generator/PackageGenerator.d.ts.map +1 -0
- package/dist/dts/generator/QueryModelsGenerator.d.ts +14 -0
- package/dist/dts/generator/QueryModelsGenerator.d.ts.map +1 -0
- package/dist/dts/generator/ServiceFnGenerator.d.ts +37 -0
- package/dist/dts/generator/ServiceFnGenerator.d.ts.map +1 -0
- package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts +40 -0
- package/dist/dts/generator/ServiceFnPromiseGenerator.d.ts.map +1 -0
- package/dist/dts/index.d.ts +101 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/model/DataModel.d.ts +188 -0
- package/dist/dts/model/DataModel.d.ts.map +1 -0
- package/dist/dts/parser/EdmxSchema.d.ts +228 -0
- package/dist/dts/parser/EdmxSchema.d.ts.map +1 -0
- package/dist/dts/parser/XmlParser.d.ts +31 -0
- package/dist/dts/parser/XmlParser.d.ts.map +1 -0
- package/dist/esm/Cli.js +87 -0
- package/dist/esm/Cli.js.map +1 -0
- package/dist/esm/bin.js +12 -0
- package/dist/esm/bin.js.map +1 -0
- package/dist/esm/digester/Digester.js +478 -0
- package/dist/esm/digester/Digester.js.map +1 -0
- package/dist/esm/digester/TypeMapper.js +248 -0
- package/dist/esm/digester/TypeMapper.js.map +1 -0
- package/dist/esm/generator/Generator.js +116 -0
- package/dist/esm/generator/Generator.js.map +1 -0
- package/dist/esm/generator/IndexGenerator.js +157 -0
- package/dist/esm/generator/IndexGenerator.js.map +1 -0
- package/dist/esm/generator/ModelsGenerator.js +248 -0
- package/dist/esm/generator/ModelsGenerator.js.map +1 -0
- package/dist/esm/generator/NamingHelper.js +147 -0
- package/dist/esm/generator/NamingHelper.js.map +1 -0
- package/dist/esm/generator/PackageGenerator.js +156 -0
- package/dist/esm/generator/PackageGenerator.js.map +1 -0
- package/dist/esm/generator/QueryModelsGenerator.js +175 -0
- package/dist/esm/generator/QueryModelsGenerator.js.map +1 -0
- package/dist/esm/generator/ServiceFnGenerator.js +251 -0
- package/dist/esm/generator/ServiceFnGenerator.js.map +1 -0
- package/dist/esm/generator/ServiceFnPromiseGenerator.js +174 -0
- package/dist/esm/generator/ServiceFnPromiseGenerator.js.map +1 -0
- package/dist/esm/index.js +101 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/model/DataModel.js +21 -0
- package/dist/esm/model/DataModel.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/parser/EdmxSchema.js +13 -0
- package/dist/esm/parser/EdmxSchema.js.map +1 -0
- package/dist/esm/parser/XmlParser.js +37 -0
- package/dist/esm/parser/XmlParser.js.map +1 -0
- package/generator/Generator/package.json +6 -0
- package/generator/IndexGenerator/package.json +6 -0
- package/generator/ModelsGenerator/package.json +6 -0
- package/generator/NamingHelper/package.json +6 -0
- package/generator/PackageGenerator/package.json +6 -0
- package/generator/QueryModelsGenerator/package.json +6 -0
- package/generator/ServiceFnGenerator/package.json +6 -0
- package/generator/ServiceFnPromiseGenerator/package.json +6 -0
- package/model/DataModel/package.json +6 -0
- package/package.json +157 -0
- package/parser/EdmxSchema/package.json +6 -0
- package/parser/XmlParser/package.json +6 -0
- package/src/Cli.ts +115 -0
- package/src/bin.ts +17 -0
- package/src/digester/Digester.ts +600 -0
- package/src/digester/TypeMapper.ts +181 -0
- package/src/generator/Generator.ts +183 -0
- package/src/generator/IndexGenerator.ts +189 -0
- package/src/generator/ModelsGenerator.ts +344 -0
- package/src/generator/NamingHelper.ts +159 -0
- package/src/generator/PackageGenerator.ts +176 -0
- package/src/generator/QueryModelsGenerator.ts +228 -0
- package/src/generator/ServiceFnGenerator.ts +324 -0
- package/src/generator/ServiceFnPromiseGenerator.ts +242 -0
- package/src/index.ts +114 -0
- package/src/model/DataModel.ts +254 -0
- package/src/parser/EdmxSchema.ts +308 -0
- 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
|
+
}
|