@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.
- package/README.md +223 -5
- package/dist/cjs/Cli.js +6 -2
- package/dist/cjs/Cli.js.map +1 -1
- package/dist/cjs/generator/Generator.js +33 -15
- package/dist/cjs/generator/Generator.js.map +1 -1
- package/dist/cjs/generator/IndexGenerator.js +19 -73
- package/dist/cjs/generator/IndexGenerator.js.map +1 -1
- package/dist/cjs/generator/NamingHelper.js +4 -1
- package/dist/cjs/generator/NamingHelper.js.map +1 -1
- package/dist/cjs/generator/NavigationGenerator.js +338 -0
- package/dist/cjs/generator/NavigationGenerator.js.map +1 -0
- package/dist/cjs/generator/OperationsGenerator.js +384 -0
- package/dist/cjs/generator/OperationsGenerator.js.map +1 -0
- package/dist/cjs/generator/QueryModelsGenerator.js +2 -2
- package/dist/cjs/generator/QueryModelsGenerator.js.map +1 -1
- package/dist/cjs/generator/ServiceFnGenerator.js +1 -1
- package/dist/cjs/generator/ServiceFnGenerator.js.map +1 -1
- package/dist/cjs/generator/ServiceFnPromiseGenerator.js +2 -2
- package/dist/cjs/generator/ServiceFnPromiseGenerator.js.map +1 -1
- package/dist/dts/Cli.d.ts.map +1 -1
- package/dist/dts/generator/Generator.d.ts +2 -0
- package/dist/dts/generator/Generator.d.ts.map +1 -1
- package/dist/dts/generator/IndexGenerator.d.ts.map +1 -1
- package/dist/dts/generator/NamingHelper.d.ts.map +1 -1
- package/dist/dts/generator/NavigationGenerator.d.ts +55 -0
- package/dist/dts/generator/NavigationGenerator.d.ts.map +1 -0
- package/dist/dts/generator/OperationsGenerator.d.ts +50 -0
- package/dist/dts/generator/OperationsGenerator.d.ts.map +1 -0
- package/dist/esm/Cli.js +6 -2
- package/dist/esm/Cli.js.map +1 -1
- package/dist/esm/generator/Generator.js +33 -15
- package/dist/esm/generator/Generator.js.map +1 -1
- package/dist/esm/generator/IndexGenerator.js +19 -73
- package/dist/esm/generator/IndexGenerator.js.map +1 -1
- package/dist/esm/generator/NamingHelper.js +4 -1
- package/dist/esm/generator/NamingHelper.js.map +1 -1
- package/dist/esm/generator/NavigationGenerator.js +330 -0
- package/dist/esm/generator/NavigationGenerator.js.map +1 -0
- package/dist/esm/generator/OperationsGenerator.js +375 -0
- package/dist/esm/generator/OperationsGenerator.js.map +1 -0
- package/dist/esm/generator/QueryModelsGenerator.js +2 -2
- package/dist/esm/generator/QueryModelsGenerator.js.map +1 -1
- package/dist/esm/generator/ServiceFnGenerator.js +1 -1
- package/dist/esm/generator/ServiceFnGenerator.js.map +1 -1
- package/dist/esm/generator/ServiceFnPromiseGenerator.js +2 -2
- package/dist/esm/generator/ServiceFnPromiseGenerator.js.map +1 -1
- package/generator/NavigationGenerator/package.json +6 -0
- package/generator/OperationsGenerator/package.json +6 -0
- package/package.json +17 -1
- package/src/Cli.ts +8 -2
- package/src/generator/Generator.ts +43 -13
- package/src/generator/IndexGenerator.ts +21 -74
- package/src/generator/NamingHelper.ts +6 -1
- package/src/generator/NavigationGenerator.ts +451 -0
- package/src/generator/OperationsGenerator.ts +481 -0
- package/src/generator/QueryModelsGenerator.ts +2 -2
- package/src/generator/ServiceFnGenerator.ts +1 -1
- 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
|
|
91
|
-
const
|
|
92
|
-
|
|
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(
|
|
107
|
+
path: path.join(sourceDir, "Models.ts"),
|
|
95
108
|
content: generateModels(dataModel)
|
|
96
109
|
},
|
|
97
110
|
{
|
|
98
|
-
path: path.join(
|
|
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(
|
|
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(
|
|
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(
|
|
137
|
+
path: path.join(sourceDir, "index.ts"),
|
|
113
138
|
content: generateIndex(dataModel)
|
|
114
|
-
}
|
|
115
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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: ${
|
|
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
|
|
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}
|
|
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}
|
|
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
|
|
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("
|
|
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`
|