wlmaker 1.2.9 → 1.2.11
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/dist/cli.mjs +191 -462
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -968,139 +968,49 @@ import * as path8 from "path";
|
|
|
968
968
|
import chalk4 from "chalk";
|
|
969
969
|
import { pascalCase as pascalCase4, camelCase } from "change-case";
|
|
970
970
|
|
|
971
|
-
// src/core/json-to-dart.ts
|
|
972
|
-
function jsonToFields(jsonStr) {
|
|
973
|
-
const parsed = JSON.parse(jsonStr);
|
|
974
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
975
|
-
throw new Error("Expected a JSON object at the top level");
|
|
976
|
-
}
|
|
977
|
-
return objectToFields(parsed);
|
|
978
|
-
}
|
|
979
|
-
function objectToFields(obj) {
|
|
980
|
-
const fields = [];
|
|
981
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
982
|
-
fields.push({
|
|
983
|
-
name: key,
|
|
984
|
-
dartType: dartTypeOf(value, key),
|
|
985
|
-
isNullable: value === null
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
return fields;
|
|
989
|
-
}
|
|
990
|
-
function dartTypeOf(value, nameHint) {
|
|
991
|
-
if (value === null) return "dynamic";
|
|
992
|
-
if (typeof value === "string") return "String";
|
|
993
|
-
if (typeof value === "boolean") return "bool";
|
|
994
|
-
if (typeof value === "number") {
|
|
995
|
-
return Number.isInteger(value) ? "int" : "double";
|
|
996
|
-
}
|
|
997
|
-
if (Array.isArray(value)) {
|
|
998
|
-
if (value.length === 0) return "List<dynamic>";
|
|
999
|
-
const elementType = dartTypeOf(value[0], nameHint);
|
|
1000
|
-
return `List<${elementType}>`;
|
|
1001
|
-
}
|
|
1002
|
-
if (typeof value === "object") {
|
|
1003
|
-
return "Map<String, dynamic>";
|
|
1004
|
-
}
|
|
1005
|
-
return "dynamic";
|
|
1006
|
-
}
|
|
1007
|
-
function fieldsToConstructorParams(fields, indent = " ") {
|
|
1008
|
-
return fields.map((f) => {
|
|
1009
|
-
const nullable = f.isNullable ? "?" : "";
|
|
1010
|
-
return `${indent}required ${f.dartType}${nullable} ${f.name},`;
|
|
1011
|
-
}).join("\n");
|
|
1012
|
-
}
|
|
1013
|
-
function fieldsToFromJson(fields, modelName) {
|
|
1014
|
-
const entries = fields.map((f) => {
|
|
1015
|
-
if (f.dartType.startsWith("List<")) {
|
|
1016
|
-
return ` ${f.name}: (${_jsonFieldName(f)} as List<dynamic>).map((e) => e as ${_extractGenericType(f.dartType)}).toList(),`;
|
|
1017
|
-
}
|
|
1018
|
-
return ` ${f.name}: ${_jsonFieldName(f)} as ${f.dartType}${f.isNullable ? "?" : ""},`;
|
|
1019
|
-
}).join("\n");
|
|
1020
|
-
return `factory ${modelName}.fromJson(Map<String, dynamic> json) => ${modelName}(
|
|
1021
|
-
${entries}
|
|
1022
|
-
);`;
|
|
1023
|
-
}
|
|
1024
|
-
function fieldsToJson(fields, modelName) {
|
|
1025
|
-
const entries = fields.map((f) => ` '${f.name}': ${f.name},`).join("\n");
|
|
1026
|
-
return `Map<String, dynamic> toJson() => {
|
|
1027
|
-
${entries}
|
|
1028
|
-
};`;
|
|
1029
|
-
}
|
|
1030
|
-
function _jsonFieldName(f) {
|
|
1031
|
-
return `json['${f.name}']`;
|
|
1032
|
-
}
|
|
1033
|
-
function _extractGenericType(listType) {
|
|
1034
|
-
const match = listType.match(/^List<(.+)>$/);
|
|
1035
|
-
return match ? match[1] : "dynamic";
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
971
|
// src/core/endpoint-templates.ts
|
|
1039
|
-
function
|
|
1040
|
-
const params = fieldsToConstructorParams(fields);
|
|
1041
|
-
const props = fields.map((f) => {
|
|
1042
|
-
const nullable = f.isNullable ? "?" : "";
|
|
1043
|
-
return ` final ${f.dartType}${nullable} ${f.name};`;
|
|
1044
|
-
}).join("\n");
|
|
972
|
+
function entityBoilerplate(pascal) {
|
|
1045
973
|
return `class ${pascal}Entity {
|
|
1046
|
-
|
|
974
|
+
// TODO: Define fields
|
|
1047
975
|
|
|
1048
|
-
const ${pascal}Entity(
|
|
1049
|
-
${params}
|
|
1050
|
-
});
|
|
976
|
+
const ${pascal}Entity();
|
|
1051
977
|
}
|
|
1052
978
|
`;
|
|
1053
979
|
}
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
const nullable = f.isNullable ? "?" : "";
|
|
1058
|
-
return ` final ${f.dartType}${nullable} ${f.name};`;
|
|
1059
|
-
}).join("\n");
|
|
1060
|
-
const fromJson = fieldsToFromJson(fields, `${pascal}Model`);
|
|
1061
|
-
const toJson = fieldsToJson(fields, `${pascal}Model`);
|
|
1062
|
-
return `import 'package:json_annotation/json_annotation.dart';
|
|
1063
|
-
import '../../domain/entities/${name}/${name}_entity.dart';
|
|
980
|
+
function modelBoilerplate(name, pascal, projectName) {
|
|
981
|
+
return `import 'package:${projectName}/core.dart';
|
|
982
|
+
import 'package:json_annotation/json_annotation.dart';
|
|
1064
983
|
|
|
1065
984
|
part '${name}_model.g.dart';
|
|
1066
985
|
|
|
1067
|
-
@JsonSerializable()
|
|
1068
|
-
class ${pascal}Model
|
|
1069
|
-
|
|
1070
|
-
${params}
|
|
1071
|
-
}) : super(
|
|
1072
|
-
${fields.map((f) => ` ${f.name}: ${f.name},`).join("\n")}
|
|
1073
|
-
);
|
|
986
|
+
@JsonSerializable(createToJson: true)
|
|
987
|
+
class ${pascal}Model {
|
|
988
|
+
// TODO: Define constructor parameters and fields
|
|
1074
989
|
|
|
1075
|
-
${
|
|
1076
|
-
|
|
1077
|
-
${toJson}
|
|
990
|
+
const ${pascal}Model();
|
|
1078
991
|
|
|
1079
992
|
factory ${pascal}Model.fromJson(Map<String, dynamic> json) =>
|
|
1080
993
|
_$${pascal}ModelFromJson(json);
|
|
1081
994
|
|
|
1082
|
-
|
|
995
|
+
${pascal}Entity toEntity() {
|
|
996
|
+
// TODO: Implement toEntity mapping
|
|
997
|
+
throw UnimplementedError();
|
|
998
|
+
}
|
|
999
|
+
|
|
1083
1000
|
Map<String, dynamic> toJson() => _$${pascal}ModelToJson(this);
|
|
1084
1001
|
}
|
|
1085
1002
|
`;
|
|
1086
1003
|
}
|
|
1087
|
-
function
|
|
1088
|
-
const params = fieldsToConstructorParams(fields);
|
|
1089
|
-
const props = fields.map((f) => {
|
|
1090
|
-
const nullable = f.isNullable ? "?" : "";
|
|
1091
|
-
return ` final ${f.dartType}${nullable} ${f.name};`;
|
|
1092
|
-
}).join("\n");
|
|
1004
|
+
function requestModelBoilerplate(name, pascal) {
|
|
1093
1005
|
return `import 'package:json_annotation/json_annotation.dart';
|
|
1094
1006
|
|
|
1095
1007
|
part '${name}_request_model.g.dart';
|
|
1096
1008
|
|
|
1097
|
-
@JsonSerializable()
|
|
1009
|
+
@JsonSerializable(createToJson: true)
|
|
1098
1010
|
class ${pascal}RequestModel {
|
|
1099
|
-
|
|
1011
|
+
// TODO: Define constructor parameters and fields
|
|
1100
1012
|
|
|
1101
|
-
${pascal}RequestModel(
|
|
1102
|
-
${params}
|
|
1103
|
-
});
|
|
1013
|
+
const ${pascal}RequestModel();
|
|
1104
1014
|
|
|
1105
1015
|
factory ${pascal}RequestModel.fromJson(Map<String, dynamic> json) =>
|
|
1106
1016
|
_$${pascal}RequestModelFromJson(json);
|
|
@@ -1156,12 +1066,13 @@ function repositoryInterfaceMethod(methodName, returnType, params) {
|
|
|
1156
1066
|
const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
|
|
1157
1067
|
return `Future<${returnType}> ${methodName}(${paramList});`;
|
|
1158
1068
|
}
|
|
1159
|
-
function repositoryImplMethod(methodName, returnType, params, datasourceName) {
|
|
1069
|
+
function repositoryImplMethod(methodName, returnType, modelType, params, datasourceName) {
|
|
1160
1070
|
const paramList = params.map((p) => `${p.type} ${p.name}`).join(", ");
|
|
1161
1071
|
const args = params.map((p) => p.name).join(", ");
|
|
1162
1072
|
return `@override
|
|
1163
1073
|
Future<${returnType}> ${methodName}(${paramList}) async {
|
|
1164
|
-
|
|
1074
|
+
final model = await ${datasourceName}.${methodName}(${args});
|
|
1075
|
+
return model.toEntity();
|
|
1165
1076
|
}`;
|
|
1166
1077
|
}
|
|
1167
1078
|
|
|
@@ -1234,102 +1145,108 @@ function injectExport(filePath, exportLine) {
|
|
|
1234
1145
|
// src/core/create-endpoint.ts
|
|
1235
1146
|
async function createEndpoint(options) {
|
|
1236
1147
|
const lib = path8.join(options.projectRoot, "lib");
|
|
1237
|
-
const pascal = pascalCase4(options.
|
|
1148
|
+
const pascal = pascalCase4(options.useCaseName);
|
|
1238
1149
|
const useCasePascal = pascalCase4(options.useCaseName);
|
|
1239
1150
|
const useCaseSnake = options.useCaseName;
|
|
1240
|
-
const
|
|
1151
|
+
const needsBody = ["POST", "PUT", "PATCH"].includes(options.httpMethod);
|
|
1152
|
+
const domain = extractDomain(options.bffApiFile);
|
|
1153
|
+
const datasourceFile = path8.join(lib, "data", "datasources", `${domain}_rest_datasource.dart`);
|
|
1154
|
+
const datasourceClassName = `${pascalCase4(domain)}RestDataSource`;
|
|
1155
|
+
const repositoryInterfaceFile = path8.join(lib, "domain", "repositories", `${domain}_repository.dart`);
|
|
1156
|
+
const repositoryInterfaceName = `${pascalCase4(domain)}Repository`;
|
|
1157
|
+
const repositoryImplFile = path8.join(lib, "data", "repositories", `${domain}_repository_data.dart`);
|
|
1158
|
+
const repositoryImplClassName = `${pascalCase4(domain)}RepositoryData`;
|
|
1159
|
+
const feature = domain;
|
|
1241
1160
|
const pathParams = extractPathParams(options.endpointPath);
|
|
1242
|
-
const methodParams = buildMethodParams(options, pathParams,
|
|
1161
|
+
const methodParams = buildMethodParams(options, pathParams, needsBody);
|
|
1243
1162
|
const methodParamsForSignature = methodParams.map((p) => ({ name: p.name, type: p.type }));
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1163
|
+
const modelType = `${pascal}Model`;
|
|
1164
|
+
const entityType = `${pascal}Entity`;
|
|
1165
|
+
const repositoryInterfaceSnake = path8.basename(repositoryInterfaceFile, ".dart");
|
|
1246
1166
|
const spinner2 = (msg) => console.log(chalk4.cyan(` \u2192 ${msg}`));
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
}
|
|
1263
|
-
if (hasRequestBody && options.requestFields && options.requestModelName) {
|
|
1167
|
+
spinner2("Generating entity");
|
|
1168
|
+
const entityDir = path8.join(lib, "domain", "entities", feature);
|
|
1169
|
+
fs9.mkdirSync(entityDir, { recursive: true });
|
|
1170
|
+
fs9.writeFileSync(
|
|
1171
|
+
path8.join(entityDir, `${useCaseSnake}_entity.dart`),
|
|
1172
|
+
entityBoilerplate(pascal)
|
|
1173
|
+
);
|
|
1174
|
+
spinner2("Generating model");
|
|
1175
|
+
const modelDir = path8.join(lib, "data", "models", feature);
|
|
1176
|
+
fs9.mkdirSync(modelDir, { recursive: true });
|
|
1177
|
+
fs9.writeFileSync(
|
|
1178
|
+
path8.join(modelDir, `${useCaseSnake}_model.dart`),
|
|
1179
|
+
modelBoilerplate(useCaseSnake, pascal, options.projectName)
|
|
1180
|
+
);
|
|
1181
|
+
if (needsBody) {
|
|
1264
1182
|
spinner2("Generating request model");
|
|
1265
|
-
const
|
|
1266
|
-
const reqModelDir = path8.join(lib, "data", "models", options.feature);
|
|
1183
|
+
const reqModelDir = path8.join(lib, "data", "models", feature);
|
|
1267
1184
|
fs9.mkdirSync(reqModelDir, { recursive: true });
|
|
1268
1185
|
fs9.writeFileSync(
|
|
1269
|
-
path8.join(reqModelDir, `${
|
|
1270
|
-
|
|
1186
|
+
path8.join(reqModelDir, `${useCaseSnake}_request_model.dart`),
|
|
1187
|
+
requestModelBoilerplate(useCaseSnake, pascal)
|
|
1271
1188
|
);
|
|
1272
1189
|
}
|
|
1273
1190
|
spinner2("Injecting Retrofit method");
|
|
1274
1191
|
const bffPath = path8.resolve(options.bffApiFile);
|
|
1275
1192
|
if (fs9.existsSync(bffPath)) {
|
|
1276
|
-
const retrofitParams = buildRetrofitParams(options, pathParams,
|
|
1277
|
-
const retrofitReturnType = `${pascal}Model`;
|
|
1193
|
+
const retrofitParams = buildRetrofitParams(options, pathParams, needsBody);
|
|
1278
1194
|
const method = retrofitMethod(
|
|
1279
1195
|
camelCase(options.useCaseName),
|
|
1280
1196
|
options.endpointPath,
|
|
1281
1197
|
options.httpMethod,
|
|
1282
1198
|
retrofitParams,
|
|
1283
|
-
|
|
1199
|
+
modelType
|
|
1284
1200
|
);
|
|
1285
1201
|
injectMethod(bffPath, extractClassName(bffPath), method);
|
|
1286
|
-
const modelImport = `import 'package:${options.
|
|
1202
|
+
const modelImport = `import 'package:${options.projectName}/data/models/${feature}/${useCaseSnake}_model.dart';`;
|
|
1287
1203
|
injectImport(bffPath, modelImport);
|
|
1288
1204
|
} else {
|
|
1289
1205
|
console.log(chalk4.yellow(` \u26A0 BFF API file not found: ${bffPath}`));
|
|
1290
1206
|
}
|
|
1291
1207
|
spinner2("Injecting datasource method");
|
|
1292
|
-
const dsPath = path8.resolve(
|
|
1208
|
+
const dsPath = path8.resolve(datasourceFile);
|
|
1293
1209
|
if (fs9.existsSync(dsPath)) {
|
|
1294
1210
|
const dsMethod = datasourceMethod(
|
|
1295
1211
|
camelCase(options.useCaseName),
|
|
1296
|
-
|
|
1212
|
+
modelType,
|
|
1297
1213
|
methodParamsForSignature
|
|
1298
1214
|
);
|
|
1299
|
-
injectMethod(dsPath,
|
|
1215
|
+
injectMethod(dsPath, datasourceClassName, dsMethod);
|
|
1300
1216
|
} else {
|
|
1301
1217
|
console.log(chalk4.yellow(` \u26A0 Datasource file not found: ${dsPath}`));
|
|
1302
1218
|
}
|
|
1303
1219
|
spinner2("Injecting repository interface method");
|
|
1304
|
-
const repoIfacePath = path8.resolve(
|
|
1220
|
+
const repoIfacePath = path8.resolve(repositoryInterfaceFile);
|
|
1305
1221
|
if (fs9.existsSync(repoIfacePath)) {
|
|
1306
1222
|
const ifaceMethod = repositoryInterfaceMethod(
|
|
1307
1223
|
camelCase(options.useCaseName),
|
|
1308
|
-
|
|
1224
|
+
entityType,
|
|
1309
1225
|
methodParamsForSignature
|
|
1310
1226
|
);
|
|
1311
|
-
injectMethod(repoIfacePath,
|
|
1312
|
-
const entityImport = `import 'package:${options.
|
|
1227
|
+
injectMethod(repoIfacePath, repositoryInterfaceName, ifaceMethod);
|
|
1228
|
+
const entityImport = `import 'package:${options.projectName}/domain/entities/${feature}/${useCaseSnake}_entity.dart';`;
|
|
1313
1229
|
injectImport(repoIfacePath, entityImport);
|
|
1314
1230
|
} else {
|
|
1315
1231
|
console.log(chalk4.yellow(` \u26A0 Repository interface not found: ${repoIfacePath}`));
|
|
1316
1232
|
}
|
|
1317
|
-
spinner2("
|
|
1318
|
-
const repoImplPath = path8.resolve(
|
|
1233
|
+
spinner2("Injecting repository implementation method");
|
|
1234
|
+
const repoImplPath = path8.resolve(repositoryImplFile);
|
|
1319
1235
|
if (fs9.existsSync(repoImplPath)) {
|
|
1320
|
-
const dsVarName = camelCase(
|
|
1236
|
+
const dsVarName = camelCase(datasourceClassName);
|
|
1321
1237
|
const implMethod = repositoryImplMethod(
|
|
1322
1238
|
camelCase(options.useCaseName),
|
|
1323
|
-
|
|
1239
|
+
entityType,
|
|
1240
|
+
modelType,
|
|
1324
1241
|
methodParamsForSignature,
|
|
1325
1242
|
dsVarName
|
|
1326
1243
|
);
|
|
1327
|
-
injectMethod(repoImplPath,
|
|
1244
|
+
injectMethod(repoImplPath, repositoryImplClassName, implMethod);
|
|
1328
1245
|
} else {
|
|
1329
1246
|
console.log(chalk4.yellow(` \u26A0 Repository implementation not found: ${repoImplPath}`));
|
|
1330
1247
|
}
|
|
1331
1248
|
spinner2("Generating UseCase");
|
|
1332
|
-
const useCaseDir = path8.join(lib, "domain", "usecases",
|
|
1249
|
+
const useCaseDir = path8.join(lib, "domain", "usecases", feature);
|
|
1333
1250
|
fs9.mkdirSync(useCaseDir, { recursive: true });
|
|
1334
1251
|
fs9.writeFileSync(
|
|
1335
1252
|
path8.join(useCaseDir, `${useCaseSnake}_usecase.dart`),
|
|
@@ -1338,27 +1255,41 @@ async function createEndpoint(options) {
|
|
|
1338
1255
|
useCasePascal,
|
|
1339
1256
|
camelCase(options.useCaseName),
|
|
1340
1257
|
methodParamsForSignature,
|
|
1341
|
-
|
|
1342
|
-
|
|
1258
|
+
entityType,
|
|
1259
|
+
repositoryInterfaceSnake
|
|
1343
1260
|
)
|
|
1344
1261
|
);
|
|
1345
|
-
spinner2("Updating barrel
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
injectExport(
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
console.log(chalk4.yellow(" \u26A0 Skipping build_runner (not found)."));
|
|
1357
|
-
}
|
|
1262
|
+
spinner2("Updating barrel files");
|
|
1263
|
+
const entityBarrel = path8.join(lib, "domain", "entities", feature, `${feature}.dart`);
|
|
1264
|
+
injectExport(entityBarrel, `export '${useCaseSnake}_entity.dart';`);
|
|
1265
|
+
injectExport(
|
|
1266
|
+
path8.join(lib, "domain", "entities", "entities.dart"),
|
|
1267
|
+
`export '${feature}/${feature}.dart';`
|
|
1268
|
+
);
|
|
1269
|
+
const modelBarrel = path8.join(lib, "data", "models", feature, `${feature}.dart`);
|
|
1270
|
+
injectExport(modelBarrel, `export '${useCaseSnake}_model.dart';`);
|
|
1271
|
+
if (needsBody) {
|
|
1272
|
+
injectExport(modelBarrel, `export '${useCaseSnake}_request_model.dart';`);
|
|
1358
1273
|
}
|
|
1274
|
+
injectExport(
|
|
1275
|
+
path8.join(lib, "data", "models", "models.dart"),
|
|
1276
|
+
`export '${feature}/${feature}.dart';`
|
|
1277
|
+
);
|
|
1278
|
+
const useCaseBarrel = path8.join(useCaseDir, `${feature}.dart`);
|
|
1279
|
+
injectExport(useCaseBarrel, `export '${useCaseSnake}_usecase.dart';`);
|
|
1280
|
+
injectExport(
|
|
1281
|
+
path8.join(lib, "domain", "usecases", "usecases.dart"),
|
|
1282
|
+
`export '${feature}/${feature}.dart';`
|
|
1283
|
+
);
|
|
1359
1284
|
console.log(chalk4.green(`
|
|
1360
1285
|
\u2713 Endpoint "${options.useCaseName}" generated successfully.`));
|
|
1361
1286
|
}
|
|
1287
|
+
function extractDomain(bffApiFile) {
|
|
1288
|
+
const baseName = path8.basename(bffApiFile);
|
|
1289
|
+
const match = baseName.match(/^bff_(.+)_api\.dart$/);
|
|
1290
|
+
if (match) return match[1];
|
|
1291
|
+
return baseName.replace(/\.dart$/, "");
|
|
1292
|
+
}
|
|
1362
1293
|
function extractPathParams(endpointPath) {
|
|
1363
1294
|
const regex = /\{(\w+)\}/g;
|
|
1364
1295
|
const params = [];
|
|
@@ -1370,8 +1301,8 @@ function extractPathParams(endpointPath) {
|
|
|
1370
1301
|
}
|
|
1371
1302
|
function buildMethodParams(options, pathParams, hasRequestBody) {
|
|
1372
1303
|
const params = [...pathParams];
|
|
1373
|
-
if (hasRequestBody
|
|
1374
|
-
const reqPascal = pascalCase4(options.
|
|
1304
|
+
if (hasRequestBody) {
|
|
1305
|
+
const reqPascal = pascalCase4(options.useCaseName);
|
|
1375
1306
|
params.push({ name: "body", type: `${reqPascal}RequestModel` });
|
|
1376
1307
|
}
|
|
1377
1308
|
return params;
|
|
@@ -1381,8 +1312,8 @@ function buildRetrofitParams(options, pathParams, hasRequestBody) {
|
|
|
1381
1312
|
for (const pp of pathParams) {
|
|
1382
1313
|
params.push({ ...pp, isPath: true });
|
|
1383
1314
|
}
|
|
1384
|
-
if (hasRequestBody
|
|
1385
|
-
const reqPascal = pascalCase4(options.
|
|
1315
|
+
if (hasRequestBody) {
|
|
1316
|
+
const reqPascal = pascalCase4(options.useCaseName);
|
|
1386
1317
|
params.push({ name: "body", type: `${reqPascal}RequestModel`, isBody: true });
|
|
1387
1318
|
}
|
|
1388
1319
|
return params;
|
|
@@ -1395,7 +1326,6 @@ function extractClassName(filePath) {
|
|
|
1395
1326
|
}
|
|
1396
1327
|
|
|
1397
1328
|
// src/interactive.ts
|
|
1398
|
-
import { pascalCase as pascalCase5 } from "change-case";
|
|
1399
1329
|
var SNAKE_CASE_REGEX4 = /^[a-z][a-z0-9_]*$/;
|
|
1400
1330
|
async function resolveProject() {
|
|
1401
1331
|
const s = clack.spinner();
|
|
@@ -1683,8 +1613,76 @@ async function useCaseFlow(project) {
|
|
|
1683
1613
|
clack.outro(chalk5.red(`Error: ${error}`));
|
|
1684
1614
|
}
|
|
1685
1615
|
}
|
|
1686
|
-
|
|
1616
|
+
function findBffFiles(dir) {
|
|
1617
|
+
const results = [];
|
|
1618
|
+
if (!fs10.existsSync(dir)) return results;
|
|
1619
|
+
const entries = fs10.readdirSync(dir, { withFileTypes: true });
|
|
1620
|
+
for (const entry of entries) {
|
|
1621
|
+
const fullPath = path9.join(dir, entry.name);
|
|
1622
|
+
if (entry.isDirectory()) {
|
|
1623
|
+
results.push(...findBffFiles(fullPath));
|
|
1624
|
+
} else if (entry.isFile() && entry.name.endsWith(".dart") && !entry.name.includes(".g.")) {
|
|
1625
|
+
results.push(fullPath);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
return results.sort();
|
|
1629
|
+
}
|
|
1630
|
+
function inferBffFile(bffFiles, endpointPath) {
|
|
1631
|
+
const firstSegment = endpointPath.replace(/^\//, "").split("/")[0].toLowerCase();
|
|
1632
|
+
if (!firstSegment) return null;
|
|
1633
|
+
const withDomains = bffFiles.map((f) => {
|
|
1634
|
+
const base = path9.basename(f, ".dart");
|
|
1635
|
+
const domain = base.replace(/^bff_/, "").replace(/_api$/, "");
|
|
1636
|
+
return { file: f, domain };
|
|
1637
|
+
});
|
|
1638
|
+
const exact = withDomains.filter((x) => x.domain === firstSegment);
|
|
1639
|
+
if (exact.length === 1) return exact[0].file;
|
|
1640
|
+
const singular = firstSegment.replace(/s$/, "");
|
|
1641
|
+
const singularMatch = withDomains.filter((x) => x.domain === singular);
|
|
1642
|
+
if (singularMatch.length === 1) return singularMatch[0].file;
|
|
1643
|
+
const endsWith = withDomains.filter(
|
|
1644
|
+
(x) => x.domain.endsWith(firstSegment) || x.domain.endsWith(singular)
|
|
1645
|
+
);
|
|
1646
|
+
if (endsWith.length === 1) return endsWith[0].file;
|
|
1647
|
+
const contains = withDomains.filter(
|
|
1648
|
+
(x) => firstSegment.includes(x.domain) || singular.includes(x.domain)
|
|
1649
|
+
);
|
|
1650
|
+
if (contains.length === 1) return contains[0].file;
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
async function resolveEndpointProject() {
|
|
1654
|
+
const s = clack.spinner();
|
|
1655
|
+
s.start("Finding BFF package...");
|
|
1656
|
+
const monorepoRoot = findMonorepoRoot(process.cwd());
|
|
1657
|
+
if (monorepoRoot) {
|
|
1658
|
+
const packages = discoverPackages(monorepoRoot);
|
|
1659
|
+
const bffPackage = packages.find(
|
|
1660
|
+
(p) => fs10.existsSync(path9.join(p.projectRoot, "lib", "data", "api", "bff"))
|
|
1661
|
+
);
|
|
1662
|
+
if (bffPackage) {
|
|
1663
|
+
s.stop(`Using ${chalk5.green(bffPackage.projectName)}`);
|
|
1664
|
+
return bffPackage;
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const cwdProject = analyzeProject(process.cwd());
|
|
1668
|
+
if (cwdProject && fs10.existsSync(path9.join(cwdProject.projectRoot, "lib", "data", "api", "bff"))) {
|
|
1669
|
+
s.stop(`Using ${chalk5.green(cwdProject.projectName)}`);
|
|
1670
|
+
return cwdProject;
|
|
1671
|
+
}
|
|
1672
|
+
s.stop("No BFF package found");
|
|
1673
|
+
clack.outro(chalk5.red("Could not find a package with lib/data/api/bff/."));
|
|
1674
|
+
return null;
|
|
1675
|
+
}
|
|
1676
|
+
async function endpointFlow() {
|
|
1677
|
+
const project = await resolveEndpointProject();
|
|
1678
|
+
if (!project) return;
|
|
1687
1679
|
const lib = path9.join(project.projectRoot, "lib");
|
|
1680
|
+
const bffDir = path9.join(lib, "data", "api", "bff");
|
|
1681
|
+
const bffFiles = findBffFiles(bffDir);
|
|
1682
|
+
if (bffFiles.length === 0) {
|
|
1683
|
+
clack.outro(chalk5.red("No BFF API files found. Ensure lib/data/api/bff/ exists with .dart files."));
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1688
1686
|
const httpMethod = await clack.select({
|
|
1689
1687
|
message: "HTTP Method",
|
|
1690
1688
|
options: [
|
|
@@ -1700,7 +1698,7 @@ async function endpointFlow(project) {
|
|
|
1700
1698
|
return;
|
|
1701
1699
|
}
|
|
1702
1700
|
const endpointPath = await clack.text({
|
|
1703
|
-
message: "Endpoint path (e.g. /
|
|
1701
|
+
message: "Endpoint path (e.g. /products/{id})",
|
|
1704
1702
|
placeholder: "/api/users/{id}",
|
|
1705
1703
|
validate: (v) => {
|
|
1706
1704
|
if (!v.trim()) return "Path is required";
|
|
@@ -1710,191 +1708,26 @@ async function endpointFlow(project) {
|
|
|
1710
1708
|
clack.cancel("Cancelled");
|
|
1711
1709
|
return;
|
|
1712
1710
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
const dartFiles = fs10.readdirSync(bffDir).filter((f) => f.endsWith(".dart")).sort();
|
|
1717
|
-
if (dartFiles.length > 0) {
|
|
1718
|
-
const selected = await clack.select({
|
|
1719
|
-
message: "Select BFF API file",
|
|
1720
|
-
options: [
|
|
1721
|
-
...dartFiles.map((f) => ({
|
|
1722
|
-
value: path9.join(bffDir, f),
|
|
1723
|
-
label: f
|
|
1724
|
-
})),
|
|
1725
|
-
{ value: "__custom__", label: "Custom path..." }
|
|
1726
|
-
]
|
|
1727
|
-
});
|
|
1728
|
-
if (clack.isCancel(selected)) {
|
|
1729
|
-
clack.cancel("Cancelled");
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
if (selected === "__custom__") {
|
|
1733
|
-
const customPath = await clack.text({
|
|
1734
|
-
message: "BFF API file path",
|
|
1735
|
-
placeholder: "lib/data/api/bff/my_api.dart",
|
|
1736
|
-
validate: (v) => {
|
|
1737
|
-
if (!v.trim()) return "Path is required";
|
|
1738
|
-
}
|
|
1739
|
-
});
|
|
1740
|
-
if (clack.isCancel(customPath)) {
|
|
1741
|
-
clack.cancel("Cancelled");
|
|
1742
|
-
return;
|
|
1743
|
-
}
|
|
1744
|
-
bffApiFile = path9.resolve(customPath);
|
|
1745
|
-
} else {
|
|
1746
|
-
bffApiFile = selected;
|
|
1747
|
-
}
|
|
1748
|
-
} else {
|
|
1749
|
-
const customPath = await clack.text({
|
|
1750
|
-
message: "BFF API file path (no .dart files found in bff/)",
|
|
1751
|
-
placeholder: "lib/data/api/bff/my_api.dart",
|
|
1752
|
-
validate: (v) => {
|
|
1753
|
-
if (!v.trim()) return "Path is required";
|
|
1754
|
-
}
|
|
1755
|
-
});
|
|
1756
|
-
if (clack.isCancel(customPath)) {
|
|
1757
|
-
clack.cancel("Cancelled");
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
bffApiFile = path9.resolve(customPath);
|
|
1761
|
-
}
|
|
1762
|
-
} else {
|
|
1763
|
-
const customPath = await clack.text({
|
|
1764
|
-
message: "BFF API file path (bff/ dir not found)",
|
|
1765
|
-
placeholder: "lib/data/api/bff/my_api.dart",
|
|
1766
|
-
validate: (v) => {
|
|
1767
|
-
if (!v.trim()) return "Path is required";
|
|
1768
|
-
}
|
|
1769
|
-
});
|
|
1770
|
-
if (clack.isCancel(customPath)) {
|
|
1771
|
-
clack.cancel("Cancelled");
|
|
1772
|
-
return;
|
|
1773
|
-
}
|
|
1774
|
-
bffApiFile = path9.resolve(customPath);
|
|
1775
|
-
}
|
|
1776
|
-
const responseChoice = await clack.select({
|
|
1777
|
-
message: "Response model",
|
|
1778
|
-
options: [
|
|
1779
|
-
{ value: "existing", label: "Use existing model", hint: "Reference an already created model" },
|
|
1780
|
-
{ value: "json", label: "Paste JSON", hint: "Generate entity + model from JSON" }
|
|
1781
|
-
]
|
|
1782
|
-
});
|
|
1783
|
-
if (clack.isCancel(responseChoice)) {
|
|
1784
|
-
clack.cancel("Cancelled");
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
let responseModelName = "";
|
|
1788
|
-
let responseFields;
|
|
1789
|
-
if (responseChoice === "json") {
|
|
1790
|
-
const modelName = await clack.text({
|
|
1791
|
-
message: "Response model name (snake_case)",
|
|
1792
|
-
placeholder: "e.g. user_profile",
|
|
1793
|
-
validate: (v) => {
|
|
1794
|
-
if (!v.trim()) return "Name is required";
|
|
1795
|
-
if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
|
|
1796
|
-
}
|
|
1797
|
-
});
|
|
1798
|
-
if (clack.isCancel(modelName)) {
|
|
1799
|
-
clack.cancel("Cancelled");
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
responseModelName = modelName;
|
|
1803
|
-
const jsonStr = await clack.text({
|
|
1804
|
-
message: "Paste the JSON response",
|
|
1805
|
-
placeholder: '{ "id": 1, "name": "John" }',
|
|
1806
|
-
validate: (v) => {
|
|
1807
|
-
if (!v.trim()) return "JSON is required";
|
|
1808
|
-
try {
|
|
1809
|
-
JSON.parse(v);
|
|
1810
|
-
} catch {
|
|
1811
|
-
return "Invalid JSON";
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
});
|
|
1815
|
-
if (clack.isCancel(jsonStr)) {
|
|
1816
|
-
clack.cancel("Cancelled");
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
responseFields = jsonToFields(jsonStr);
|
|
1711
|
+
let bffApiFile = inferBffFile(bffFiles, endpointPath);
|
|
1712
|
+
if (bffApiFile) {
|
|
1713
|
+
clack.log.info(`Auto-detected BFF file: ${chalk5.cyan(path9.basename(bffApiFile))}`);
|
|
1820
1714
|
} else {
|
|
1821
|
-
const
|
|
1822
|
-
message: "
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1715
|
+
const selected = await clack.select({
|
|
1716
|
+
message: "Could not auto-detect BFF file. Select one:",
|
|
1717
|
+
options: bffFiles.map((f) => ({
|
|
1718
|
+
value: f,
|
|
1719
|
+
label: path9.relative(bffDir, f)
|
|
1720
|
+
}))
|
|
1827
1721
|
});
|
|
1828
|
-
if (clack.isCancel(
|
|
1722
|
+
if (clack.isCancel(selected)) {
|
|
1829
1723
|
clack.cancel("Cancelled");
|
|
1830
1724
|
return;
|
|
1831
1725
|
}
|
|
1832
|
-
|
|
1833
|
-
}
|
|
1834
|
-
let requestModelName;
|
|
1835
|
-
let requestFields;
|
|
1836
|
-
const needsBody = ["POST", "PUT", "PATCH"].includes(httpMethod);
|
|
1837
|
-
if (needsBody) {
|
|
1838
|
-
const requestChoice = await clack.select({
|
|
1839
|
-
message: "Request body",
|
|
1840
|
-
options: [
|
|
1841
|
-
{ value: "json", label: "Paste JSON", hint: "Generate request model from JSON" },
|
|
1842
|
-
{ value: "existing", label: "Use existing model", hint: "Reference an already created model" },
|
|
1843
|
-
{ value: "none", label: "No body", hint: "Just path/query params" }
|
|
1844
|
-
]
|
|
1845
|
-
});
|
|
1846
|
-
if (clack.isCancel(requestChoice)) {
|
|
1847
|
-
clack.cancel("Cancelled");
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
if (requestChoice === "json") {
|
|
1851
|
-
const reqName = await clack.text({
|
|
1852
|
-
message: "Request model name (snake_case)",
|
|
1853
|
-
placeholder: `e.g. create_${responseModelName}`,
|
|
1854
|
-
validate: (v) => {
|
|
1855
|
-
if (!v.trim()) return "Name is required";
|
|
1856
|
-
if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
|
|
1857
|
-
}
|
|
1858
|
-
});
|
|
1859
|
-
if (clack.isCancel(reqName)) {
|
|
1860
|
-
clack.cancel("Cancelled");
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
requestModelName = reqName;
|
|
1864
|
-
const reqJson = await clack.text({
|
|
1865
|
-
message: "Paste the request JSON body",
|
|
1866
|
-
placeholder: '{ "name": "John", "email": "john@example.com" }',
|
|
1867
|
-
validate: (v) => {
|
|
1868
|
-
if (!v.trim()) return "JSON is required";
|
|
1869
|
-
try {
|
|
1870
|
-
JSON.parse(v);
|
|
1871
|
-
} catch {
|
|
1872
|
-
return "Invalid JSON";
|
|
1873
|
-
}
|
|
1874
|
-
}
|
|
1875
|
-
});
|
|
1876
|
-
if (clack.isCancel(reqJson)) {
|
|
1877
|
-
clack.cancel("Cancelled");
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
requestFields = jsonToFields(reqJson);
|
|
1881
|
-
} else if (requestChoice === "existing") {
|
|
1882
|
-
const reqName = await clack.text({
|
|
1883
|
-
message: "Existing request model name (snake_case)",
|
|
1884
|
-
placeholder: `e.g. create_${responseModelName}`,
|
|
1885
|
-
validate: (v) => {
|
|
1886
|
-
if (!v.trim()) return "Name is required";
|
|
1887
|
-
}
|
|
1888
|
-
});
|
|
1889
|
-
if (clack.isCancel(reqName)) {
|
|
1890
|
-
clack.cancel("Cancelled");
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
requestModelName = reqName;
|
|
1894
|
-
}
|
|
1726
|
+
bffApiFile = selected;
|
|
1895
1727
|
}
|
|
1728
|
+
const methodLower = httpMethod.toLowerCase();
|
|
1896
1729
|
const pathSegments = endpointPath.replace(/^\//, "").split("/").filter((s) => !s.startsWith("{"));
|
|
1897
|
-
const inferredName = pathSegments.length > 0 ?
|
|
1730
|
+
const inferredName = pathSegments.length > 0 ? `${methodLower}_${pathSegments.join("_")}` : `${methodLower}_data`;
|
|
1898
1731
|
const useCaseName = await clack.text({
|
|
1899
1732
|
message: "UseCase name (snake_case)",
|
|
1900
1733
|
placeholder: inferredName,
|
|
@@ -1908,117 +1741,16 @@ async function endpointFlow(project) {
|
|
|
1908
1741
|
clack.cancel("Cancelled");
|
|
1909
1742
|
return;
|
|
1910
1743
|
}
|
|
1911
|
-
const datasourceFile = await clack.text({
|
|
1912
|
-
message: "Datasource file path",
|
|
1913
|
-
placeholder: `lib/data/datasources/${responseModelName}_datasource.dart`,
|
|
1914
|
-
validate: (v) => {
|
|
1915
|
-
if (!v.trim()) return "Path is required";
|
|
1916
|
-
}
|
|
1917
|
-
});
|
|
1918
|
-
if (clack.isCancel(datasourceFile)) {
|
|
1919
|
-
clack.cancel("Cancelled");
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
const datasourceClassName = await clack.text({
|
|
1923
|
-
message: "Datasource class name",
|
|
1924
|
-
placeholder: `${pascalCase5(responseModelName)}Datasource`,
|
|
1925
|
-
initialValue: `${pascalCase5(responseModelName)}Datasource`,
|
|
1926
|
-
validate: (v) => {
|
|
1927
|
-
if (!v.trim()) return "Class name is required";
|
|
1928
|
-
}
|
|
1929
|
-
});
|
|
1930
|
-
if (clack.isCancel(datasourceClassName)) {
|
|
1931
|
-
clack.cancel("Cancelled");
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
|
-
const repositoryInterfaceFile = await clack.text({
|
|
1935
|
-
message: "Repository interface file path",
|
|
1936
|
-
placeholder: `lib/domain/repositories/${responseModelName}_repository.dart`,
|
|
1937
|
-
validate: (v) => {
|
|
1938
|
-
if (!v.trim()) return "Path is required";
|
|
1939
|
-
}
|
|
1940
|
-
});
|
|
1941
|
-
if (clack.isCancel(repositoryInterfaceFile)) {
|
|
1942
|
-
clack.cancel("Cancelled");
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
const repositoryInterfaceName2 = await clack.text({
|
|
1946
|
-
message: "Repository interface name",
|
|
1947
|
-
placeholder: `${pascalCase5(responseModelName)}Repository`,
|
|
1948
|
-
initialValue: `${pascalCase5(responseModelName)}Repository`,
|
|
1949
|
-
validate: (v) => {
|
|
1950
|
-
if (!v.trim()) return "Name is required";
|
|
1951
|
-
}
|
|
1952
|
-
});
|
|
1953
|
-
if (clack.isCancel(repositoryInterfaceName2)) {
|
|
1954
|
-
clack.cancel("Cancelled");
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
const repositoryImplFile = await clack.text({
|
|
1958
|
-
message: "Repository implementation file path",
|
|
1959
|
-
placeholder: `lib/data/repositories/${responseModelName}_repository_impl.dart`,
|
|
1960
|
-
validate: (v) => {
|
|
1961
|
-
if (!v.trim()) return "Path is required";
|
|
1962
|
-
}
|
|
1963
|
-
});
|
|
1964
|
-
if (clack.isCancel(repositoryImplFile)) {
|
|
1965
|
-
clack.cancel("Cancelled");
|
|
1966
|
-
return;
|
|
1967
|
-
}
|
|
1968
|
-
const repositoryImplClassName = await clack.text({
|
|
1969
|
-
message: "Repository implementation class name",
|
|
1970
|
-
placeholder: `${pascalCase5(responseModelName)}RepositoryImpl`,
|
|
1971
|
-
initialValue: `${pascalCase5(responseModelName)}RepositoryImpl`,
|
|
1972
|
-
validate: (v) => {
|
|
1973
|
-
if (!v.trim()) return "Name is required";
|
|
1974
|
-
}
|
|
1975
|
-
});
|
|
1976
|
-
if (clack.isCancel(repositoryImplClassName)) {
|
|
1977
|
-
clack.cancel("Cancelled");
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
const feature = await clack.text({
|
|
1981
|
-
message: "Feature name (snake_case, for directory grouping)",
|
|
1982
|
-
placeholder: responseModelName,
|
|
1983
|
-
initialValue: responseModelName,
|
|
1984
|
-
validate: (v) => {
|
|
1985
|
-
if (!v.trim()) return "Feature name is required";
|
|
1986
|
-
if (!SNAKE_CASE_REGEX4.test(v)) return "Must be snake_case";
|
|
1987
|
-
}
|
|
1988
|
-
});
|
|
1989
|
-
if (clack.isCancel(feature)) {
|
|
1990
|
-
clack.cancel("Cancelled");
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
const runBuildRunner2 = await clack.confirm({
|
|
1994
|
-
message: "Run build_runner after generation?",
|
|
1995
|
-
initialValue: project.hasBuildRunner
|
|
1996
|
-
});
|
|
1997
|
-
if (clack.isCancel(runBuildRunner2)) {
|
|
1998
|
-
clack.cancel("Cancelled");
|
|
1999
|
-
return;
|
|
2000
|
-
}
|
|
2001
1744
|
const genSpinner = clack.spinner();
|
|
2002
1745
|
genSpinner.start("Generating endpoint stack...");
|
|
2003
1746
|
try {
|
|
2004
1747
|
await createEndpoint({
|
|
2005
1748
|
projectRoot: project.projectRoot,
|
|
1749
|
+
projectName: project.projectName,
|
|
2006
1750
|
httpMethod,
|
|
2007
1751
|
endpointPath,
|
|
2008
1752
|
bffApiFile,
|
|
2009
|
-
|
|
2010
|
-
responseFields,
|
|
2011
|
-
requestModelName,
|
|
2012
|
-
requestFields,
|
|
2013
|
-
useCaseName,
|
|
2014
|
-
datasourceFile,
|
|
2015
|
-
datasourceClassName,
|
|
2016
|
-
repositoryInterfaceFile,
|
|
2017
|
-
repositoryInterfaceName: repositoryInterfaceName2,
|
|
2018
|
-
repositoryImplFile,
|
|
2019
|
-
repositoryImplClassName,
|
|
2020
|
-
feature,
|
|
2021
|
-
runBuildRunner: runBuildRunner2
|
|
1753
|
+
useCaseName
|
|
2022
1754
|
});
|
|
2023
1755
|
genSpinner.stop("Endpoint generated");
|
|
2024
1756
|
clack.outro(chalk5.green("Done!"));
|
|
@@ -2067,12 +1799,9 @@ async function interactiveMode() {
|
|
|
2067
1799
|
await useCaseFlow(project);
|
|
2068
1800
|
break;
|
|
2069
1801
|
}
|
|
2070
|
-
case "endpoint":
|
|
2071
|
-
|
|
2072
|
-
if (!project) return;
|
|
2073
|
-
await endpointFlow(project);
|
|
1802
|
+
case "endpoint":
|
|
1803
|
+
await endpointFlow();
|
|
2074
1804
|
break;
|
|
2075
|
-
}
|
|
2076
1805
|
}
|
|
2077
1806
|
}
|
|
2078
1807
|
|