@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,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator for Models.ts - Effect Schema definitions.
|
|
3
|
+
*
|
|
4
|
+
* @since 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
ComplexTypeModel,
|
|
8
|
+
DataModel,
|
|
9
|
+
EntityTypeModel,
|
|
10
|
+
EnumTypeModel,
|
|
11
|
+
NavigationPropertyModel,
|
|
12
|
+
PropertyModel
|
|
13
|
+
} from "../model/DataModel.js"
|
|
14
|
+
import { getClassName, getEditableTypeName, getIdTypeName } from "./NamingHelper.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get dependencies for a type (complex types it references via properties or baseType).
|
|
18
|
+
*/
|
|
19
|
+
const getTypeDependencies = (
|
|
20
|
+
type: ComplexTypeModel | EntityTypeModel,
|
|
21
|
+
allComplexTypes: Set<string>,
|
|
22
|
+
allEntityTypes: Set<string>
|
|
23
|
+
): Set<string> => {
|
|
24
|
+
const deps = new Set<string>()
|
|
25
|
+
|
|
26
|
+
// Add base type dependency if it exists
|
|
27
|
+
if (type.baseType) {
|
|
28
|
+
const baseTypeName = getClassName(type.baseType.split(".").pop() ?? type.baseType)
|
|
29
|
+
if (allComplexTypes.has(baseTypeName) || allEntityTypes.has(baseTypeName)) {
|
|
30
|
+
deps.add(baseTypeName)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add property type dependencies (for complex types referenced in properties)
|
|
35
|
+
for (const prop of type.properties) {
|
|
36
|
+
const propTypeName = prop.typeMapping.effectSchema
|
|
37
|
+
if (allComplexTypes.has(propTypeName)) {
|
|
38
|
+
deps.add(propTypeName)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return deps
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Topologically sort types based on their dependencies.
|
|
47
|
+
*/
|
|
48
|
+
const sortTypesByDependency = <T extends ComplexTypeModel | EntityTypeModel>(
|
|
49
|
+
types: Array<T>,
|
|
50
|
+
allComplexTypes: Set<string>,
|
|
51
|
+
allEntityTypes: Set<string>
|
|
52
|
+
): Array<T> => {
|
|
53
|
+
const sorted: Array<T> = []
|
|
54
|
+
const visited = new Set<string>()
|
|
55
|
+
const visiting = new Set<string>()
|
|
56
|
+
|
|
57
|
+
const visit = (type: T) => {
|
|
58
|
+
if (visited.has(type.name)) return
|
|
59
|
+
if (visiting.has(type.name)) {
|
|
60
|
+
// Circular dependency - just add it
|
|
61
|
+
visited.add(type.name)
|
|
62
|
+
sorted.push(type)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
visiting.add(type.name)
|
|
67
|
+
|
|
68
|
+
const deps = getTypeDependencies(type, allComplexTypes, allEntityTypes)
|
|
69
|
+
for (const depName of deps) {
|
|
70
|
+
const depType = types.find((t) => t.name === depName)
|
|
71
|
+
if (depType && !visited.has(depName)) {
|
|
72
|
+
visit(depType)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
visiting.delete(type.name)
|
|
77
|
+
visited.add(type.name)
|
|
78
|
+
sorted.push(type)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (const type of types) {
|
|
82
|
+
visit(type)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return sorted
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate the Models.ts file content.
|
|
90
|
+
*
|
|
91
|
+
* @since 1.0.0
|
|
92
|
+
* @category generation
|
|
93
|
+
*/
|
|
94
|
+
export const generateModels = (dataModel: DataModel): string => {
|
|
95
|
+
const lines: Array<string> = []
|
|
96
|
+
|
|
97
|
+
// Collect all type names for dependency resolution
|
|
98
|
+
const allComplexTypes = new Set(Array.from(dataModel.complexTypes.values()).map((t) => t.name))
|
|
99
|
+
const allEntityTypes = new Set(Array.from(dataModel.entityTypes.values()).map((t) => t.name))
|
|
100
|
+
|
|
101
|
+
// Header
|
|
102
|
+
lines.push(`/**`)
|
|
103
|
+
lines.push(` * Effect Schema models for ${dataModel.serviceName} OData service.`)
|
|
104
|
+
lines.push(` * Generated by odata-effect-gen.`)
|
|
105
|
+
lines.push(` *`)
|
|
106
|
+
lines.push(` * @since 1.0.0`)
|
|
107
|
+
lines.push(` */`)
|
|
108
|
+
lines.push(`import * as Schema from "effect/Schema"`)
|
|
109
|
+
lines.push(``)
|
|
110
|
+
|
|
111
|
+
// Generate enum types (no dependencies, always first)
|
|
112
|
+
for (const enumType of dataModel.enumTypes.values()) {
|
|
113
|
+
for (const line of generateEnumType(enumType)) lines.push(line)
|
|
114
|
+
lines.push(``)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Generate complex types in dependency order
|
|
118
|
+
const sortedComplexTypes = sortTypesByDependency(
|
|
119
|
+
Array.from(dataModel.complexTypes.values()),
|
|
120
|
+
allComplexTypes,
|
|
121
|
+
allEntityTypes
|
|
122
|
+
)
|
|
123
|
+
for (const complexType of sortedComplexTypes) {
|
|
124
|
+
for (const line of generateComplexType(complexType, dataModel)) lines.push(line)
|
|
125
|
+
lines.push(``)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Generate entity types in dependency order
|
|
129
|
+
const sortedEntityTypes = sortTypesByDependency(
|
|
130
|
+
Array.from(dataModel.entityTypes.values()),
|
|
131
|
+
allComplexTypes,
|
|
132
|
+
allEntityTypes
|
|
133
|
+
)
|
|
134
|
+
for (const entityType of sortedEntityTypes) {
|
|
135
|
+
for (const line of generateEntityType(entityType, dataModel)) lines.push(line)
|
|
136
|
+
lines.push(``)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return lines.join("\n")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate an enum type.
|
|
144
|
+
*/
|
|
145
|
+
const generateEnumType = (enumType: EnumTypeModel): Array<string> => {
|
|
146
|
+
const lines: Array<string> = []
|
|
147
|
+
const members = enumType.members.map((m) => `"${m.name}"`).join(", ")
|
|
148
|
+
|
|
149
|
+
lines.push(`/**`)
|
|
150
|
+
lines.push(` * ${enumType.odataName} enum type.`)
|
|
151
|
+
lines.push(` *`)
|
|
152
|
+
lines.push(` * @since 1.0.0`)
|
|
153
|
+
lines.push(` * @category enums`)
|
|
154
|
+
lines.push(` */`)
|
|
155
|
+
lines.push(`export const ${enumType.name} = Schema.Literal(${members})`)
|
|
156
|
+
lines.push(`export type ${enumType.name} = Schema.Schema.Type<typeof ${enumType.name}>`)
|
|
157
|
+
|
|
158
|
+
return lines
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Generate a complex type.
|
|
163
|
+
*/
|
|
164
|
+
const generateComplexType = (
|
|
165
|
+
complexType: ComplexTypeModel,
|
|
166
|
+
dataModel: DataModel
|
|
167
|
+
): Array<string> => {
|
|
168
|
+
const lines: Array<string> = []
|
|
169
|
+
|
|
170
|
+
lines.push(`/**`)
|
|
171
|
+
lines.push(` * ${complexType.odataName} complex type.`)
|
|
172
|
+
lines.push(` *`)
|
|
173
|
+
lines.push(` * @since 1.0.0`)
|
|
174
|
+
lines.push(` * @category models`)
|
|
175
|
+
lines.push(` */`)
|
|
176
|
+
|
|
177
|
+
const fields = generateSchemaFields(complexType.properties, complexType.navigationProperties, dataModel)
|
|
178
|
+
|
|
179
|
+
lines.push(`export class ${complexType.name} extends Schema.Class<${complexType.name}>("${complexType.name}")({`)
|
|
180
|
+
for (const f of fields) lines.push(` ${f}`)
|
|
181
|
+
lines.push(`}) {}`)
|
|
182
|
+
|
|
183
|
+
// Generate editable type
|
|
184
|
+
lines.push(``)
|
|
185
|
+
lines.push(`/**`)
|
|
186
|
+
lines.push(` * Editable ${complexType.odataName} for creating/updating operations.`)
|
|
187
|
+
lines.push(` *`)
|
|
188
|
+
lines.push(` * @since 1.0.0`)
|
|
189
|
+
lines.push(` * @category models`)
|
|
190
|
+
lines.push(` */`)
|
|
191
|
+
|
|
192
|
+
const editableFields = generateEditableSchemaFields(complexType.properties)
|
|
193
|
+
const editableName = getEditableTypeName(complexType.name)
|
|
194
|
+
|
|
195
|
+
lines.push(`export const ${editableName} = Schema.Struct({`)
|
|
196
|
+
for (const f of editableFields) lines.push(` ${f}`)
|
|
197
|
+
lines.push(`})`)
|
|
198
|
+
lines.push(`export type ${editableName} = Schema.Schema.Type<typeof ${editableName}>`)
|
|
199
|
+
|
|
200
|
+
return lines
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Generate an entity type with ID and editable variants.
|
|
205
|
+
*/
|
|
206
|
+
const generateEntityType = (
|
|
207
|
+
entityType: EntityTypeModel,
|
|
208
|
+
dataModel: DataModel
|
|
209
|
+
): Array<string> => {
|
|
210
|
+
const lines: Array<string> = []
|
|
211
|
+
|
|
212
|
+
// Main entity class
|
|
213
|
+
lines.push(`/**`)
|
|
214
|
+
lines.push(` * ${entityType.odataName} entity type.`)
|
|
215
|
+
lines.push(` *`)
|
|
216
|
+
lines.push(` * @since 1.0.0`)
|
|
217
|
+
lines.push(` * @category models`)
|
|
218
|
+
lines.push(` */`)
|
|
219
|
+
|
|
220
|
+
const fields = generateSchemaFields(entityType.properties, entityType.navigationProperties, dataModel)
|
|
221
|
+
|
|
222
|
+
lines.push(`export class ${entityType.name} extends Schema.Class<${entityType.name}>("${entityType.name}")({`)
|
|
223
|
+
for (const f of fields) lines.push(` ${f}`)
|
|
224
|
+
lines.push(`}) {}`)
|
|
225
|
+
|
|
226
|
+
// ID type
|
|
227
|
+
if (entityType.keys.length > 0) {
|
|
228
|
+
lines.push(``)
|
|
229
|
+
lines.push(`/**`)
|
|
230
|
+
lines.push(` * ${entityType.odataName} ID type.`)
|
|
231
|
+
lines.push(` *`)
|
|
232
|
+
lines.push(` * @since 1.0.0`)
|
|
233
|
+
lines.push(` * @category models`)
|
|
234
|
+
lines.push(` */`)
|
|
235
|
+
|
|
236
|
+
const idTypeName = getIdTypeName(entityType.name)
|
|
237
|
+
|
|
238
|
+
if (entityType.keys.length === 1) {
|
|
239
|
+
const key = entityType.keys[0]
|
|
240
|
+
const keySchema = getPropertySchemaType(key, false)
|
|
241
|
+
lines.push(`export const ${idTypeName} = Schema.Union(`)
|
|
242
|
+
lines.push(` ${keySchema},`)
|
|
243
|
+
lines.push(` Schema.Struct({ ${key.name}: ${keySchema} })`)
|
|
244
|
+
lines.push(`)`)
|
|
245
|
+
} else {
|
|
246
|
+
// Composite key
|
|
247
|
+
const keyFields = entityType.keys.map((k) => {
|
|
248
|
+
const schema = getPropertySchemaType(k, false)
|
|
249
|
+
return `${k.name}: ${schema}`
|
|
250
|
+
})
|
|
251
|
+
lines.push(`export const ${idTypeName} = Schema.Union(`)
|
|
252
|
+
lines.push(` Schema.String,`)
|
|
253
|
+
lines.push(` Schema.Struct({ ${keyFields.join(", ")} })`)
|
|
254
|
+
lines.push(`)`)
|
|
255
|
+
}
|
|
256
|
+
lines.push(`export type ${idTypeName} = Schema.Schema.Type<typeof ${idTypeName}>`)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Editable type
|
|
260
|
+
lines.push(``)
|
|
261
|
+
lines.push(`/**`)
|
|
262
|
+
lines.push(` * Editable ${entityType.odataName} for creating/updating operations.`)
|
|
263
|
+
lines.push(` *`)
|
|
264
|
+
lines.push(` * @since 1.0.0`)
|
|
265
|
+
lines.push(` * @category models`)
|
|
266
|
+
lines.push(` */`)
|
|
267
|
+
|
|
268
|
+
const editableFields = generateEditableSchemaFields(
|
|
269
|
+
entityType.properties.filter((p) => !p.isKey)
|
|
270
|
+
)
|
|
271
|
+
const editableName = getEditableTypeName(entityType.name)
|
|
272
|
+
|
|
273
|
+
lines.push(`export const ${editableName} = Schema.Struct({`)
|
|
274
|
+
for (const f of editableFields) lines.push(` ${f}`)
|
|
275
|
+
lines.push(`})`)
|
|
276
|
+
lines.push(`export type ${editableName} = Schema.Schema.Type<typeof ${editableName}>`)
|
|
277
|
+
|
|
278
|
+
return lines
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Generate schema field definitions for a class.
|
|
283
|
+
* Note: Navigation properties are excluded to avoid circular reference issues.
|
|
284
|
+
* They can be loaded via $expand queries.
|
|
285
|
+
*/
|
|
286
|
+
const generateSchemaFields = (
|
|
287
|
+
properties: ReadonlyArray<PropertyModel>,
|
|
288
|
+
_navigationProperties: ReadonlyArray<NavigationPropertyModel>,
|
|
289
|
+
_dataModel: DataModel
|
|
290
|
+
): Array<string> => {
|
|
291
|
+
const fields: Array<string> = []
|
|
292
|
+
|
|
293
|
+
for (let i = 0; i < properties.length; i++) {
|
|
294
|
+
const prop = properties[i]
|
|
295
|
+
const schemaType = getPropertySchemaType(prop, prop.isNullable && !prop.isKey)
|
|
296
|
+
const isLast = i === properties.length - 1
|
|
297
|
+
fields.push(`${prop.name}: ${schemaType}${isLast ? "" : ","}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Navigation properties are intentionally excluded from Schema.Class
|
|
301
|
+
// to avoid circular reference issues. They can be loaded via $expand.
|
|
302
|
+
|
|
303
|
+
return fields
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Generate schema field definitions for an editable struct.
|
|
308
|
+
*/
|
|
309
|
+
const generateEditableSchemaFields = (
|
|
310
|
+
properties: ReadonlyArray<PropertyModel>
|
|
311
|
+
): Array<string> => {
|
|
312
|
+
const fields: Array<string> = []
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < properties.length; i++) {
|
|
315
|
+
const prop = properties[i]
|
|
316
|
+
const schemaType = getPropertySchemaType(prop, prop.isNullable)
|
|
317
|
+
const isLast = i === properties.length - 1
|
|
318
|
+
fields.push(`${prop.name}: ${schemaType}${isLast ? "" : ","}`)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return fields
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the Schema type for a property.
|
|
326
|
+
*/
|
|
327
|
+
const getPropertySchemaType = (
|
|
328
|
+
prop: PropertyModel,
|
|
329
|
+
makeOptional: boolean
|
|
330
|
+
): string => {
|
|
331
|
+
let baseType = prop.typeMapping.effectSchema
|
|
332
|
+
|
|
333
|
+
// Handle collection
|
|
334
|
+
if (prop.isCollection) {
|
|
335
|
+
baseType = `Schema.Array(${baseType})`
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Handle nullable/optional
|
|
339
|
+
if (makeOptional) {
|
|
340
|
+
return `Schema.optionalWith(${baseType}, { nullable: true })`
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return baseType
|
|
344
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Naming utilities for code generation.
|
|
3
|
+
*
|
|
4
|
+
* @since 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert a string to PascalCase.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* toPascalCase("hello_world") // "HelloWorld"
|
|
12
|
+
* toPascalCase("helloWorld") // "HelloWorld"
|
|
13
|
+
*
|
|
14
|
+
* @since 1.0.0
|
|
15
|
+
* @category naming
|
|
16
|
+
*/
|
|
17
|
+
export const toPascalCase = (str: string): string => {
|
|
18
|
+
return str
|
|
19
|
+
.replace(/[-_](.)/g, (_, char) => char.toUpperCase())
|
|
20
|
+
.replace(/^(.)/, (char) => char.toUpperCase())
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a string to camelCase.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* toCamelCase("HelloWorld") // "helloWorld"
|
|
28
|
+
* toCamelCase("hello_world") // "helloWorld"
|
|
29
|
+
*
|
|
30
|
+
* @since 1.0.0
|
|
31
|
+
* @category naming
|
|
32
|
+
*/
|
|
33
|
+
export const toCamelCase = (str: string): string => {
|
|
34
|
+
const pascal = toPascalCase(str)
|
|
35
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Ensure a string is a valid TypeScript identifier.
|
|
40
|
+
* Prefixes with underscore if it starts with a number.
|
|
41
|
+
*
|
|
42
|
+
* @since 1.0.0
|
|
43
|
+
* @category naming
|
|
44
|
+
*/
|
|
45
|
+
export const toValidIdentifier = (str: string): string => {
|
|
46
|
+
// If starts with a number, prefix with underscore
|
|
47
|
+
if (/^[0-9]/.test(str)) {
|
|
48
|
+
return `_${str}`
|
|
49
|
+
}
|
|
50
|
+
// Replace invalid characters with underscores
|
|
51
|
+
return str.replace(/[^a-zA-Z0-9_$]/g, "_")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the TypeScript property name for an OData property.
|
|
56
|
+
*
|
|
57
|
+
* @since 1.0.0
|
|
58
|
+
* @category naming
|
|
59
|
+
*/
|
|
60
|
+
export const getPropertyName = (odataName: string): string => {
|
|
61
|
+
return toCamelCase(toValidIdentifier(odataName))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the TypeScript class name for an OData entity or complex type.
|
|
66
|
+
*
|
|
67
|
+
* @since 1.0.0
|
|
68
|
+
* @category naming
|
|
69
|
+
*/
|
|
70
|
+
export const getClassName = (odataName: string): string => {
|
|
71
|
+
return toPascalCase(toValidIdentifier(odataName))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get the service class name for an entity set.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* getServiceClassName("Products") // "ProductService"
|
|
79
|
+
*
|
|
80
|
+
* @since 1.0.0
|
|
81
|
+
* @category naming
|
|
82
|
+
*/
|
|
83
|
+
export const getServiceClassName = (entitySetName: string): string => {
|
|
84
|
+
// Remove trailing 's' if present to singularize
|
|
85
|
+
let singular = entitySetName
|
|
86
|
+
if (singular.endsWith("ies")) {
|
|
87
|
+
singular = singular.slice(0, -3) + "y"
|
|
88
|
+
} else if (singular.endsWith("es") && !singular.endsWith("ses")) {
|
|
89
|
+
singular = singular.slice(0, -2)
|
|
90
|
+
} else if (singular.endsWith("s") && !singular.endsWith("ss")) {
|
|
91
|
+
singular = singular.slice(0, -1)
|
|
92
|
+
}
|
|
93
|
+
return `${toPascalCase(singular)}Service`
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the query paths interface name for a type.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* getQueryInterfaceName("Product") // "QProduct"
|
|
101
|
+
*
|
|
102
|
+
* @since 1.0.0
|
|
103
|
+
* @category naming
|
|
104
|
+
*/
|
|
105
|
+
export const getQueryInterfaceName = (typeName: string): string => {
|
|
106
|
+
return `Q${toPascalCase(typeName)}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get the query paths instance name for a type.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* getQueryInstanceName("Product") // "qProduct"
|
|
114
|
+
*
|
|
115
|
+
* @since 1.0.0
|
|
116
|
+
* @category naming
|
|
117
|
+
*/
|
|
118
|
+
export const getQueryInstanceName = (typeName: string): string => {
|
|
119
|
+
return `q${toPascalCase(typeName)}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the query builder factory function name for a type.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* getQueryFactoryName("Product") // "productQuery"
|
|
127
|
+
*
|
|
128
|
+
* @since 1.0.0
|
|
129
|
+
* @category naming
|
|
130
|
+
*/
|
|
131
|
+
export const getQueryFactoryName = (typeName: string): string => {
|
|
132
|
+
return `${toCamelCase(typeName)}Query`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get the editable type name for an entity type.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* getEditableTypeName("Product") // "EditableProduct"
|
|
140
|
+
*
|
|
141
|
+
* @since 1.0.0
|
|
142
|
+
* @category naming
|
|
143
|
+
*/
|
|
144
|
+
export const getEditableTypeName = (typeName: string): string => {
|
|
145
|
+
return `Editable${toPascalCase(typeName)}`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get the ID type name for an entity type.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* getIdTypeName("Product") // "ProductId"
|
|
153
|
+
*
|
|
154
|
+
* @since 1.0.0
|
|
155
|
+
* @category naming
|
|
156
|
+
*/
|
|
157
|
+
export const getIdTypeName = (typeName: string): string => {
|
|
158
|
+
return `${toPascalCase(typeName)}Id`
|
|
159
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generator for package configuration files.
|
|
3
|
+
*
|
|
4
|
+
* @since 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
import type { DataModel } from "../model/DataModel.js"
|
|
7
|
+
|
|
8
|
+
export interface PackageConfig {
|
|
9
|
+
readonly packageName: string
|
|
10
|
+
readonly serviceName: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate package.json content.
|
|
15
|
+
*
|
|
16
|
+
* @since 1.0.0
|
|
17
|
+
* @category generation
|
|
18
|
+
*/
|
|
19
|
+
export const generatePackageJson = (
|
|
20
|
+
dataModel: DataModel,
|
|
21
|
+
config: PackageConfig
|
|
22
|
+
): string => {
|
|
23
|
+
const packageJson = {
|
|
24
|
+
name: config.packageName,
|
|
25
|
+
version: "0.0.0",
|
|
26
|
+
type: "module",
|
|
27
|
+
license: "MIT",
|
|
28
|
+
description: `Effect-based OData client for ${dataModel.serviceName} service`,
|
|
29
|
+
publishConfig: {
|
|
30
|
+
access: "public",
|
|
31
|
+
directory: "dist"
|
|
32
|
+
},
|
|
33
|
+
scripts: {
|
|
34
|
+
codegen: "build-utils prepare-v2",
|
|
35
|
+
build: "pnpm build-esm && pnpm build-annotate && pnpm build-cjs && build-utils pack-v2",
|
|
36
|
+
"build-esm": "tsc -b tsconfig.build.json",
|
|
37
|
+
"build-cjs":
|
|
38
|
+
"babel build/esm --plugins @babel/transform-export-namespace-from --plugins @babel/transform-modules-commonjs --out-dir build/cjs --source-maps",
|
|
39
|
+
"build-annotate": "babel build/esm --plugins annotate-pure-calls --out-dir build/esm --source-maps",
|
|
40
|
+
check: "tsc -b tsconfig.json",
|
|
41
|
+
test: "vitest",
|
|
42
|
+
coverage: "vitest --coverage"
|
|
43
|
+
},
|
|
44
|
+
dependencies: {
|
|
45
|
+
"@effect/platform": "^0.94.0",
|
|
46
|
+
"@odata-effect/odata-effect": "workspace:*",
|
|
47
|
+
"@odata-effect/odata-effect-promise": "workspace:*",
|
|
48
|
+
effect: "^3.10.7"
|
|
49
|
+
},
|
|
50
|
+
devDependencies: {
|
|
51
|
+
"@odata-effect/odata-effect": "workspace:*",
|
|
52
|
+
"@odata-effect/odata-effect-promise": "workspace:*"
|
|
53
|
+
},
|
|
54
|
+
effect: {
|
|
55
|
+
generateExports: {
|
|
56
|
+
include: ["**/*.ts"]
|
|
57
|
+
},
|
|
58
|
+
generateIndex: {
|
|
59
|
+
include: ["**/*.ts"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return JSON.stringify(packageJson, null, 2)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate tsconfig.json content.
|
|
69
|
+
*
|
|
70
|
+
* @since 1.0.0
|
|
71
|
+
* @category generation
|
|
72
|
+
*/
|
|
73
|
+
export const generateTsconfig = (): string => {
|
|
74
|
+
const tsconfig = {
|
|
75
|
+
extends: "../../tsconfig.base.json",
|
|
76
|
+
include: [],
|
|
77
|
+
references: [
|
|
78
|
+
{ path: "tsconfig.src.json" },
|
|
79
|
+
{ path: "tsconfig.test.json" }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return JSON.stringify(tsconfig, null, 2)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generate tsconfig.src.json content.
|
|
88
|
+
*
|
|
89
|
+
* @since 1.0.0
|
|
90
|
+
* @category generation
|
|
91
|
+
*/
|
|
92
|
+
export const generateTsconfigSrc = (): string => {
|
|
93
|
+
const tsconfig = {
|
|
94
|
+
extends: "../../tsconfig.base.json",
|
|
95
|
+
include: ["src"],
|
|
96
|
+
references: [
|
|
97
|
+
{ path: "../ODataEffect/tsconfig.src.json" },
|
|
98
|
+
{ path: "../ODataEffectPromise/tsconfig.src.json" }
|
|
99
|
+
],
|
|
100
|
+
compilerOptions: {
|
|
101
|
+
types: ["node"],
|
|
102
|
+
outDir: "build/src",
|
|
103
|
+
tsBuildInfoFile: ".tsbuildinfo/src.tsbuildinfo",
|
|
104
|
+
rootDir: "src"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return JSON.stringify(tsconfig, null, 2)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Generate tsconfig.test.json content.
|
|
113
|
+
*
|
|
114
|
+
* @since 1.0.0
|
|
115
|
+
* @category generation
|
|
116
|
+
*/
|
|
117
|
+
export const generateTsconfigTest = (): string => {
|
|
118
|
+
const tsconfig = {
|
|
119
|
+
extends: "../../tsconfig.base.json",
|
|
120
|
+
include: ["test"],
|
|
121
|
+
references: [
|
|
122
|
+
{ path: "tsconfig.src.json" },
|
|
123
|
+
{ path: "../ODataEffect/tsconfig.src.json" },
|
|
124
|
+
{ path: "../ODataEffectPromise/tsconfig.src.json" }
|
|
125
|
+
],
|
|
126
|
+
compilerOptions: {
|
|
127
|
+
types: ["node"],
|
|
128
|
+
tsBuildInfoFile: ".tsbuildinfo/test.tsbuildinfo",
|
|
129
|
+
rootDir: "test",
|
|
130
|
+
noEmit: true
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return JSON.stringify(tsconfig, null, 2)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Generate tsconfig.build.json content.
|
|
139
|
+
*
|
|
140
|
+
* @since 1.0.0
|
|
141
|
+
* @category generation
|
|
142
|
+
*/
|
|
143
|
+
export const generateTsconfigBuild = (): string => {
|
|
144
|
+
const tsconfig = {
|
|
145
|
+
extends: "./tsconfig.src.json",
|
|
146
|
+
references: [
|
|
147
|
+
{ path: "../ODataEffect/tsconfig.build.json" },
|
|
148
|
+
{ path: "../ODataEffectPromise/tsconfig.build.json" }
|
|
149
|
+
],
|
|
150
|
+
compilerOptions: {
|
|
151
|
+
types: ["node"],
|
|
152
|
+
tsBuildInfoFile: ".tsbuildinfo/build.tsbuildinfo",
|
|
153
|
+
outDir: "build/esm",
|
|
154
|
+
declarationDir: "build/dts",
|
|
155
|
+
stripInternal: true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return JSON.stringify(tsconfig, null, 2)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Generate vitest.config.ts content.
|
|
164
|
+
*
|
|
165
|
+
* @since 1.0.0
|
|
166
|
+
* @category generation
|
|
167
|
+
*/
|
|
168
|
+
export const generateVitestConfig = (): string => {
|
|
169
|
+
return `import { mergeConfig, type UserConfigExport } from "vitest/config"
|
|
170
|
+
import shared from "../../vitest.shared.js"
|
|
171
|
+
|
|
172
|
+
const config: UserConfigExport = {}
|
|
173
|
+
|
|
174
|
+
export default mergeConfig(shared, config)
|
|
175
|
+
`
|
|
176
|
+
}
|