@ingenyus/swarm-wasp 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/LICENSE +23 -0
- package/README.md +34 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/common/filesystem.d.ts +87 -0
- package/dist/common/filesystem.d.ts.map +1 -0
- package/dist/common/index.d.ts +6 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/plugin.d.ts +2 -0
- package/dist/common/plugin.d.ts.map +1 -0
- package/dist/common/prisma.d.ts +80 -0
- package/dist/common/prisma.d.ts.map +1 -0
- package/dist/common/schemas.d.ts +50 -0
- package/dist/common/schemas.d.ts.map +1 -0
- package/dist/common/templates.d.ts +15 -0
- package/dist/common/templates.d.ts.map +1 -0
- package/dist/generators/action/action-generator.d.ts +36 -0
- package/dist/generators/action/action-generator.d.ts.map +1 -0
- package/dist/generators/action/index.d.ts +2 -0
- package/dist/generators/action/index.d.ts.map +1 -0
- package/dist/generators/action/schema.d.ts +27 -0
- package/dist/generators/action/schema.d.ts.map +1 -0
- package/dist/generators/api/api-generator.d.ts +37 -0
- package/dist/generators/api/api-generator.d.ts.map +1 -0
- package/dist/generators/api/index.d.ts +2 -0
- package/dist/generators/api/index.d.ts.map +1 -0
- package/dist/generators/api/schema.d.ts +26 -0
- package/dist/generators/api/schema.d.ts.map +1 -0
- package/dist/generators/api/templates/api.eta +6 -0
- package/dist/generators/api/templates/config/api.eta +11 -0
- package/dist/generators/api-namespace/api-namespace-generator.d.ts +28 -0
- package/dist/generators/api-namespace/api-namespace-generator.d.ts.map +1 -0
- package/dist/generators/api-namespace/index.d.ts +2 -0
- package/dist/generators/api-namespace/index.d.ts.map +1 -0
- package/dist/generators/api-namespace/schema.d.ts +16 -0
- package/dist/generators/api-namespace/schema.d.ts.map +1 -0
- package/dist/generators/api-namespace/templates/config/api-namespace.eta +3 -0
- package/dist/generators/api-namespace/templates/middleware.eta +19 -0
- package/dist/generators/args.types.d.ts +85 -0
- package/dist/generators/args.types.d.ts.map +1 -0
- package/dist/generators/base/entity-generator.base.d.ts +57 -0
- package/dist/generators/base/entity-generator.base.d.ts.map +1 -0
- package/dist/generators/base/index.d.ts +3 -0
- package/dist/generators/base/index.d.ts.map +1 -0
- package/dist/generators/base/operation-generator.base.d.ts +60 -0
- package/dist/generators/base/operation-generator.base.d.ts.map +1 -0
- package/dist/generators/base/wasp-generator.base.d.ts +47 -0
- package/dist/generators/base/wasp-generator.base.d.ts.map +1 -0
- package/dist/generators/config/config-generator.d.ts +17 -0
- package/dist/generators/config/config-generator.d.ts.map +1 -0
- package/dist/generators/config/index.d.ts +3 -0
- package/dist/generators/config/index.d.ts.map +1 -0
- package/dist/generators/config/templates/api-namespace.eta +3 -0
- package/dist/generators/config/templates/api.eta +11 -0
- package/dist/generators/config/templates/crud.eta +4 -0
- package/dist/generators/config/templates/job.eta +9 -0
- package/dist/generators/config/templates/operation.eta +6 -0
- package/dist/generators/config/templates/route.eta +4 -0
- package/dist/generators/config/wasp-config-generator.d.ts +92 -0
- package/dist/generators/config/wasp-config-generator.d.ts.map +1 -0
- package/dist/generators/crud/crud-generator.d.ts +40 -0
- package/dist/generators/crud/crud-generator.d.ts.map +1 -0
- package/dist/generators/crud/index.d.ts +2 -0
- package/dist/generators/crud/index.d.ts.map +1 -0
- package/dist/generators/crud/schema.d.ts +22 -0
- package/dist/generators/crud/schema.d.ts.map +1 -0
- package/dist/generators/crud/templates/config/crud.eta +4 -0
- package/dist/generators/crud/templates/crud.eta +2 -0
- package/dist/generators/feature-directory/feature-directory-generator.d.ts +18 -0
- package/dist/generators/feature-directory/feature-directory-generator.d.ts.map +1 -0
- package/dist/generators/feature-directory/index.d.ts +2 -0
- package/dist/generators/feature-directory/index.d.ts.map +1 -0
- package/dist/generators/feature-directory/schema.d.ts +8 -0
- package/dist/generators/feature-directory/schema.d.ts.map +1 -0
- package/dist/generators/feature-directory/templates/feature.wasp.eta +22 -0
- package/dist/generators/index.d.ts +10 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/job/index.d.ts +2 -0
- package/dist/generators/job/index.d.ts.map +1 -0
- package/dist/generators/job/job-generator.d.ts +35 -0
- package/dist/generators/job/job-generator.d.ts.map +1 -0
- package/dist/generators/job/schema.d.ts +22 -0
- package/dist/generators/job/schema.d.ts.map +1 -0
- package/dist/generators/job/templates/config/job.eta +9 -0
- package/dist/generators/job/templates/job.eta +5 -0
- package/dist/generators/operation/templates/config/operation.eta +6 -0
- package/dist/generators/operation/templates/create.eta +17 -0
- package/dist/generators/operation/templates/delete.eta +21 -0
- package/dist/generators/operation/templates/get.eta +19 -0
- package/dist/generators/operation/templates/getAll.eta +13 -0
- package/dist/generators/operation/templates/getFiltered.eta +17 -0
- package/dist/generators/operation/templates/update.eta +26 -0
- package/dist/generators/query/index.d.ts +2 -0
- package/dist/generators/query/index.d.ts.map +1 -0
- package/dist/generators/query/query-generator.d.ts +36 -0
- package/dist/generators/query/query-generator.d.ts.map +1 -0
- package/dist/generators/query/schema.d.ts +27 -0
- package/dist/generators/query/schema.d.ts.map +1 -0
- package/dist/generators/route/index.d.ts +2 -0
- package/dist/generators/route/index.d.ts.map +1 -0
- package/dist/generators/route/route-generator.d.ts +32 -0
- package/dist/generators/route/route-generator.d.ts.map +1 -0
- package/dist/generators/route/schema.d.ts +19 -0
- package/dist/generators/route/schema.d.ts.map +1 -0
- package/dist/generators/route/templates/config/route.eta +4 -0
- package/dist/generators/route/templates/page.eta +10 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2718 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/types/constants.d.ts +102 -0
- package/dist/types/constants.d.ts.map +1 -0
- package/dist/types/constants.js +63 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +63 -0
- package/dist/types/prisma.types.d.ts +23 -0
- package/dist/types/prisma.types.d.ts.map +1 -0
- package/dist/types/prisma.types.js +0 -0
- package/dist/wasp-config/app.d.ts +146 -0
- package/dist/wasp-config/app.d.ts.map +1 -0
- package/dist/wasp-config/index.d.ts +3 -0
- package/dist/wasp-config/index.d.ts.map +1 -0
- package/dist/wasp-config/stubs/index.d.ts +180 -0
- package/dist/wasp-config/stubs/index.d.ts.map +1 -0
- package/package.json +92 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2718 @@
|
|
|
1
|
+
// src/common/filesystem.ts
|
|
2
|
+
import { toPascalCase, validateFeaturePath } from "@ingenyus/swarm";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
// src/types/constants.ts
|
|
8
|
+
var PLUGIN_NAME = "wasp";
|
|
9
|
+
var OPERATION_TYPES = ["query", "action"];
|
|
10
|
+
var HTTP_METHODS = ["ALL", "GET", "POST", "PUT", "DELETE"];
|
|
11
|
+
var OPERATIONS = {
|
|
12
|
+
CREATE: "create",
|
|
13
|
+
UPDATE: "update",
|
|
14
|
+
DELETE: "delete",
|
|
15
|
+
GET: "get",
|
|
16
|
+
GETALL: "getAll",
|
|
17
|
+
GETFILTERED: "getFiltered"
|
|
18
|
+
};
|
|
19
|
+
var CRUD_OPERATIONS = {
|
|
20
|
+
CREATE: "create",
|
|
21
|
+
GET: "get",
|
|
22
|
+
GETALL: "getAll",
|
|
23
|
+
UPDATE: "update",
|
|
24
|
+
DELETE: "delete"
|
|
25
|
+
};
|
|
26
|
+
var ACTION_OPERATIONS = {
|
|
27
|
+
CREATE: "create",
|
|
28
|
+
UPDATE: "update",
|
|
29
|
+
DELETE: "delete"
|
|
30
|
+
};
|
|
31
|
+
var QUERY_OPERATIONS = {
|
|
32
|
+
GET: "get",
|
|
33
|
+
GETALL: "getAll",
|
|
34
|
+
GETFILTERED: "getFiltered"
|
|
35
|
+
};
|
|
36
|
+
var TYPE_DIRECTORIES = {
|
|
37
|
+
component: "client/components",
|
|
38
|
+
hook: "client/hooks",
|
|
39
|
+
layout: "client/layouts",
|
|
40
|
+
page: "client/pages",
|
|
41
|
+
util: "client/utils",
|
|
42
|
+
action: "server/actions",
|
|
43
|
+
query: "server/queries",
|
|
44
|
+
middleware: "server/middleware",
|
|
45
|
+
job: "server/jobs",
|
|
46
|
+
api: "server/apis",
|
|
47
|
+
crud: "server/cruds",
|
|
48
|
+
type: "types"
|
|
49
|
+
};
|
|
50
|
+
var CONFIG_TYPES = {
|
|
51
|
+
ROUTE: "Route",
|
|
52
|
+
QUERY: "Query",
|
|
53
|
+
ACTION: "Action",
|
|
54
|
+
JOB: "Job",
|
|
55
|
+
API: "Api",
|
|
56
|
+
API_NAMESPACE: "ApiNamespace",
|
|
57
|
+
CRUD: "Crud"
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/common/filesystem.ts
|
|
61
|
+
var realFileSystem = {
|
|
62
|
+
readFileSync: fs.readFileSync,
|
|
63
|
+
writeFileSync: fs.writeFileSync,
|
|
64
|
+
existsSync: fs.existsSync,
|
|
65
|
+
copyFileSync: fs.copyFileSync,
|
|
66
|
+
mkdirSync: fs.mkdirSync,
|
|
67
|
+
readdirSync: fs.readdirSync,
|
|
68
|
+
statSync: fs.statSync
|
|
69
|
+
};
|
|
70
|
+
function findWaspRoot(fileSystem, startDir = process.cwd()) {
|
|
71
|
+
const startDirPath = path.resolve(startDir);
|
|
72
|
+
let currentDirPath = startDirPath;
|
|
73
|
+
const root = path.parse(currentDirPath).root;
|
|
74
|
+
while (currentDirPath !== root) {
|
|
75
|
+
const waspRootPath = path.join(currentDirPath, ".wasproot");
|
|
76
|
+
if (fileSystem.existsSync(waspRootPath)) {
|
|
77
|
+
return currentDirPath;
|
|
78
|
+
}
|
|
79
|
+
currentDirPath = path.dirname(currentDirPath);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(
|
|
82
|
+
`Couldn't find Wasp application root from ${startDirPath}. Make sure you are running this command from within a Wasp project directory.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
function copyDirectory(fileSystem, src, dest) {
|
|
86
|
+
if (!fileSystem.existsSync(dest)) {
|
|
87
|
+
fileSystem.mkdirSync(dest, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
const entries = fileSystem.readdirSync(src, { withFileTypes: true });
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
const srcPath = path.join(src, entry.name);
|
|
92
|
+
const destPath = path.join(dest, entry.name);
|
|
93
|
+
if (entry.isDirectory()) {
|
|
94
|
+
copyDirectory(fileSystem, srcPath, destPath);
|
|
95
|
+
} else {
|
|
96
|
+
fileSystem.copyFileSync(srcPath, destPath);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function ensureDirectoryExists(fileSystem, dir) {
|
|
101
|
+
if (!fileSystem.existsSync(dir)) {
|
|
102
|
+
fileSystem.mkdirSync(dir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function featureExists(fileSystem, featurePath) {
|
|
106
|
+
return fileSystem.existsSync(getFeatureDir(fileSystem, featurePath));
|
|
107
|
+
}
|
|
108
|
+
function getConfigDir(fileSystem) {
|
|
109
|
+
const waspRoot = findWaspRoot(fileSystem);
|
|
110
|
+
return path.join(waspRoot, "config");
|
|
111
|
+
}
|
|
112
|
+
function normaliseFeaturePath(featurePath) {
|
|
113
|
+
const segments = validateFeaturePath(featurePath);
|
|
114
|
+
const normalisedSegments = [];
|
|
115
|
+
for (let i = 0; i < segments.length; i++) {
|
|
116
|
+
const segment = segments[i];
|
|
117
|
+
const previousSegment = normalisedSegments[normalisedSegments.length - 1];
|
|
118
|
+
if (previousSegment !== "features" && segment !== "features") {
|
|
119
|
+
normalisedSegments.push("features");
|
|
120
|
+
}
|
|
121
|
+
normalisedSegments.push(segment);
|
|
122
|
+
}
|
|
123
|
+
return normalisedSegments.join("/");
|
|
124
|
+
}
|
|
125
|
+
function getFeatureDir(fileSystem, featureName) {
|
|
126
|
+
const waspRoot = findWaspRoot(fileSystem);
|
|
127
|
+
const normalisedPath = normaliseFeaturePath(featureName);
|
|
128
|
+
return path.join(waspRoot, "src", normalisedPath);
|
|
129
|
+
}
|
|
130
|
+
function getFeatureImportPath(featurePath) {
|
|
131
|
+
const segments = validateFeaturePath(featurePath);
|
|
132
|
+
return segments.join("/");
|
|
133
|
+
}
|
|
134
|
+
function getFeatureTargetDir(fileSystem, featurePath, type) {
|
|
135
|
+
validateFeaturePath(featurePath);
|
|
136
|
+
const normalisedPath = normaliseFeaturePath(featurePath);
|
|
137
|
+
const featureDir = getFeatureDir(fileSystem, normalisedPath);
|
|
138
|
+
const typeKey = type.toLowerCase();
|
|
139
|
+
const typeDirectory = TYPE_DIRECTORIES[typeKey];
|
|
140
|
+
const targetDirectory = path.join(featureDir, typeDirectory);
|
|
141
|
+
const importDirectory = `@src/${normalisedPath}/${typeDirectory}`;
|
|
142
|
+
return { targetDirectory, importDirectory };
|
|
143
|
+
}
|
|
144
|
+
function getRouteNameFromPath(routePath) {
|
|
145
|
+
const lastSegment = routePath.split("/").filter(Boolean).pop() || "index";
|
|
146
|
+
const cleanSegment = lastSegment.replace(/[:*]/g, "");
|
|
147
|
+
return `${toPascalCase(cleanSegment)}Page`;
|
|
148
|
+
}
|
|
149
|
+
function getAppRootDir(fileSystem) {
|
|
150
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
151
|
+
let currentDir = path.dirname(currentFile);
|
|
152
|
+
while (currentDir !== path.dirname(currentDir)) {
|
|
153
|
+
const dirName = path.basename(currentDir);
|
|
154
|
+
if (dirName === "src" || dirName === "dist") {
|
|
155
|
+
return currentDir;
|
|
156
|
+
}
|
|
157
|
+
const packageJsonPath = path.join(currentDir, "package.json");
|
|
158
|
+
if (fileSystem.existsSync(packageJsonPath)) {
|
|
159
|
+
const srcPath = path.join(currentDir, "src");
|
|
160
|
+
const distPath = path.join(currentDir, "dist");
|
|
161
|
+
if (fileSystem.existsSync(distPath)) {
|
|
162
|
+
return distPath;
|
|
163
|
+
} else if (fileSystem.existsSync(srcPath)) {
|
|
164
|
+
return srcPath;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
currentDir = path.dirname(currentDir);
|
|
168
|
+
}
|
|
169
|
+
throw new Error("Could not determine application root directory");
|
|
170
|
+
}
|
|
171
|
+
function getTemplatesDir(fileSystem) {
|
|
172
|
+
const appRoot = getAppRootDir(fileSystem);
|
|
173
|
+
return path.join(appRoot, "templates");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/common/plugin.ts
|
|
177
|
+
import path2 from "path";
|
|
178
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
179
|
+
function getPluginVersion() {
|
|
180
|
+
const __dirname = path2.dirname(fileURLToPath2(import.meta.url));
|
|
181
|
+
let currentDir = __dirname;
|
|
182
|
+
while (currentDir !== path2.dirname(currentDir)) {
|
|
183
|
+
const packageJsonPath = path2.join(currentDir, "package.json");
|
|
184
|
+
if (realFileSystem.existsSync(packageJsonPath)) {
|
|
185
|
+
try {
|
|
186
|
+
const packageJson = JSON.parse(
|
|
187
|
+
realFileSystem.readFileSync(packageJsonPath, "utf8")
|
|
188
|
+
);
|
|
189
|
+
if (packageJson.name === "@ingenyus/swarm-wasp") {
|
|
190
|
+
return packageJson.version;
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
currentDir = path2.dirname(currentDir);
|
|
196
|
+
}
|
|
197
|
+
return "0.1.0";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/common/prisma.ts
|
|
201
|
+
import {
|
|
202
|
+
getSchema
|
|
203
|
+
} from "@mrleebo/prisma-ast";
|
|
204
|
+
import fs2 from "fs";
|
|
205
|
+
import path3 from "path";
|
|
206
|
+
async function getEntityMetadata(modelName) {
|
|
207
|
+
try {
|
|
208
|
+
const schemaPath = path3.join(process.cwd(), "schema.prisma");
|
|
209
|
+
const schemaContent = fs2.readFileSync(schemaPath, "utf8");
|
|
210
|
+
const schema9 = getSchema(schemaContent);
|
|
211
|
+
const model = schema9.list?.find(
|
|
212
|
+
(m) => m.type === "model" && m.name === modelName
|
|
213
|
+
);
|
|
214
|
+
if (!model || model.type !== "model") {
|
|
215
|
+
throw new Error(`Model ${modelName} not found in schema`);
|
|
216
|
+
}
|
|
217
|
+
const compositeIdAttr = (model.properties || []).find(
|
|
218
|
+
(item) => item.type === "attribute" && item.kind === "object" && item.name === "id"
|
|
219
|
+
);
|
|
220
|
+
let compositeIdFields = [];
|
|
221
|
+
if (compositeIdAttr?.args?.[0]) {
|
|
222
|
+
const arg = compositeIdAttr.args[0];
|
|
223
|
+
if (typeof arg.value === "object" && arg.value !== null && "type" in arg.value && arg.value.type === "array" && "args" in arg.value) {
|
|
224
|
+
compositeIdFields = arg.value.args;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const fields = (model.properties || []).filter(
|
|
228
|
+
(item) => item.type === "field" && !item.array && !item.attributes?.some((attr) => attr.name === "relation")
|
|
229
|
+
).map((field) => {
|
|
230
|
+
const fieldType = typeof field.fieldType === "string" ? field.fieldType : field.fieldType.name;
|
|
231
|
+
const tsType = getPrismaToTsType(fieldType);
|
|
232
|
+
const isRequired = !field.optional;
|
|
233
|
+
const isId = field.attributes?.some((attr) => attr.name === "id") || compositeIdFields.includes(field.name);
|
|
234
|
+
const isUnique = field.attributes?.some((attr) => attr.name === "unique") || false;
|
|
235
|
+
const hasDefaultValue = field.attributes?.some((attr) => attr.name === "default") || false;
|
|
236
|
+
const isUpdatedAt = field.attributes?.some((attr) => attr.name === "updatedAt") || false;
|
|
237
|
+
const isGenerated = field.attributes?.some((attr) => attr.name === "map") || false;
|
|
238
|
+
return {
|
|
239
|
+
name: field.name,
|
|
240
|
+
type: fieldType,
|
|
241
|
+
tsType,
|
|
242
|
+
isRequired,
|
|
243
|
+
isId,
|
|
244
|
+
isUnique,
|
|
245
|
+
hasDefaultValue,
|
|
246
|
+
isGenerated,
|
|
247
|
+
isUpdatedAt
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
return {
|
|
251
|
+
name: modelName,
|
|
252
|
+
fields
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Failed to get entity metadata for ${modelName}: ${error instanceof Error ? error.message : String(error)}`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function getIdFields(model) {
|
|
261
|
+
const idFields = model.fields.filter((f) => f.isId).map((f) => f.name);
|
|
262
|
+
if (idFields.length === 0) {
|
|
263
|
+
throw new Error(`No ID field found for model ${model.name}`);
|
|
264
|
+
}
|
|
265
|
+
return idFields;
|
|
266
|
+
}
|
|
267
|
+
function getRequiredFields(model) {
|
|
268
|
+
return model.fields.filter(
|
|
269
|
+
(f) => f.isRequired && !f.hasDefaultValue && !f.isGenerated && !f.isUpdatedAt
|
|
270
|
+
).map((f) => f.name);
|
|
271
|
+
}
|
|
272
|
+
function getOptionalFields(model) {
|
|
273
|
+
return model.fields.filter(
|
|
274
|
+
(field) => (field.hasDefaultValue && field.type !== "DateTime" || !field.isRequired) && !field.isId && !field.isGenerated && !field.isUpdatedAt
|
|
275
|
+
).map((field) => field.name);
|
|
276
|
+
}
|
|
277
|
+
function getJsonFields(model) {
|
|
278
|
+
return model.fields.filter((f) => f.type === "Json").map((f) => f.name);
|
|
279
|
+
}
|
|
280
|
+
function generateJsonTypeHandling(jsonFields) {
|
|
281
|
+
if (jsonFields.length === 0) return "";
|
|
282
|
+
const assignments = jsonFields.map(
|
|
283
|
+
(field) => ` ${field}: (data.${field} as Prisma.JsonValue) || Prisma.JsonNull`
|
|
284
|
+
).join(",\n");
|
|
285
|
+
return `,
|
|
286
|
+
${assignments}`;
|
|
287
|
+
}
|
|
288
|
+
function needsPrismaImport(model) {
|
|
289
|
+
return model.fields.some((f) => f.type === "Json" || f.type === "Decimal");
|
|
290
|
+
}
|
|
291
|
+
function generatePickType(modelName, fields, allFields) {
|
|
292
|
+
if (fields.length === 0) return "";
|
|
293
|
+
if (fields.length === allFields.length) return modelName;
|
|
294
|
+
const fieldUnion = fields.map((f) => `"${f}"`).join(" | ");
|
|
295
|
+
return `Pick<${modelName}, ${fieldUnion}>`;
|
|
296
|
+
}
|
|
297
|
+
function generateOmitType(modelName, fields, allFields) {
|
|
298
|
+
if (fields.length === 0) return modelName;
|
|
299
|
+
if (fields.length === allFields.length) return "";
|
|
300
|
+
const fieldUnion = fields.map((f) => `"${f}"`).join(" | ");
|
|
301
|
+
return `Omit<${modelName}, ${fieldUnion}>`;
|
|
302
|
+
}
|
|
303
|
+
function generatePartialType(typeString) {
|
|
304
|
+
if (!typeString) return "";
|
|
305
|
+
return `Partial<${typeString}>`;
|
|
306
|
+
}
|
|
307
|
+
function generateIntersectionType(type1, type2) {
|
|
308
|
+
if (!type1 && !type2) return "";
|
|
309
|
+
if (!type1) return type2;
|
|
310
|
+
if (!type2) return type1;
|
|
311
|
+
return `${type1} & ${type2}`;
|
|
312
|
+
}
|
|
313
|
+
function getPrismaToTsType(type) {
|
|
314
|
+
const typeMap = {
|
|
315
|
+
String: "string",
|
|
316
|
+
Int: "number",
|
|
317
|
+
Float: "number",
|
|
318
|
+
Boolean: "boolean",
|
|
319
|
+
DateTime: "Date",
|
|
320
|
+
Json: "Prisma.JsonValue",
|
|
321
|
+
BigInt: "bigint",
|
|
322
|
+
Decimal: "Prisma.Decimal",
|
|
323
|
+
Bytes: "Buffer"
|
|
324
|
+
};
|
|
325
|
+
return typeMap[type] || type;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/common/schemas.ts
|
|
329
|
+
import { extend } from "@ingenyus/swarm";
|
|
330
|
+
import { z } from "zod";
|
|
331
|
+
var commonSchemas = {
|
|
332
|
+
feature: extend(z.string().min(1, "Feature is required"), {
|
|
333
|
+
description: "The feature directory this resource will be generated in",
|
|
334
|
+
friendlyName: "Feature",
|
|
335
|
+
shortName: "f",
|
|
336
|
+
examples: ["root", "auth", "dashboard/users"],
|
|
337
|
+
helpText: 'Can be nested as a logical or relative path, e.g. "dashboard/users" or "features/dashboard/features/users"'
|
|
338
|
+
}),
|
|
339
|
+
name: extend(z.string().min(1, "Name is required"), {
|
|
340
|
+
description: "The name of the generated resource",
|
|
341
|
+
friendlyName: "Name",
|
|
342
|
+
shortName: "n",
|
|
343
|
+
examples: ["users", "task"],
|
|
344
|
+
helpText: "Will be used for generated files and configuration entries"
|
|
345
|
+
}),
|
|
346
|
+
path: extend(z.string().min(1, "Path is required"), {
|
|
347
|
+
description: "The path that this resource will be accessible at",
|
|
348
|
+
friendlyName: "Path",
|
|
349
|
+
shortName: "p",
|
|
350
|
+
examples: ["/api/users/:id", "/api/products"],
|
|
351
|
+
helpText: 'Supports Express-style placeholders, e.g. "/api/users/:id"'
|
|
352
|
+
}),
|
|
353
|
+
entities: extend(z.string().optional(), {
|
|
354
|
+
description: "The Wasp entities that this resource will have access to (optional)",
|
|
355
|
+
friendlyName: "Entities",
|
|
356
|
+
shortName: "e",
|
|
357
|
+
examples: ["User,Product"],
|
|
358
|
+
helpText: "A comma-separated list of Wasp entities"
|
|
359
|
+
}),
|
|
360
|
+
force: extend(z.boolean().optional(), {
|
|
361
|
+
description: "Force overwrite of existing files and configuration entries (optional)",
|
|
362
|
+
friendlyName: "Force",
|
|
363
|
+
shortName: "F",
|
|
364
|
+
helpText: "CAUTION: Will overwrite existing files and configuration entries with current parameters"
|
|
365
|
+
}),
|
|
366
|
+
auth: extend(z.boolean().optional(), {
|
|
367
|
+
description: "Require authentication for this resource (optional)",
|
|
368
|
+
friendlyName: "Auth",
|
|
369
|
+
shortName: "a",
|
|
370
|
+
helpText: "Will generate authentication checks"
|
|
371
|
+
})
|
|
372
|
+
};
|
|
373
|
+
var getTypedArrayValidator = (validValues) => {
|
|
374
|
+
return (input) => {
|
|
375
|
+
if (!input) return true;
|
|
376
|
+
const values = input.split(",").map((s) => s.trim()).filter(Boolean);
|
|
377
|
+
const normalisedValues = validValues.map((value) => value.toLowerCase());
|
|
378
|
+
for (const value of values) {
|
|
379
|
+
const normalisedValue = value.toLowerCase();
|
|
380
|
+
if (!normalisedValues.includes(normalisedValue)) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
var getTypedArrayTransformer = (validValues) => {
|
|
388
|
+
return (input) => {
|
|
389
|
+
if (!input) return void 0;
|
|
390
|
+
return input.split(",").map((s) => s.trim()).filter(Boolean).map((value) => {
|
|
391
|
+
const normalisedValue = value.toLowerCase();
|
|
392
|
+
const validValue = validValues.find(
|
|
393
|
+
(val) => val.toLowerCase() === normalisedValue
|
|
394
|
+
);
|
|
395
|
+
return validValue;
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
};
|
|
399
|
+
var getTypedValueValidator = (validValues) => {
|
|
400
|
+
return (value) => {
|
|
401
|
+
const normalisedValue = value.toLowerCase();
|
|
402
|
+
const normalisedValidOps = validValues.map((val) => val.toLowerCase());
|
|
403
|
+
return normalisedValidOps.includes(normalisedValue);
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
var getTypedValueTransformer = (validValues) => {
|
|
407
|
+
return (value) => {
|
|
408
|
+
const normalisedValue = value.toLowerCase();
|
|
409
|
+
const validValue = validValues.find(
|
|
410
|
+
(val) => val.toLowerCase() === normalisedValue
|
|
411
|
+
);
|
|
412
|
+
return validValue;
|
|
413
|
+
};
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// src/common/templates.ts
|
|
417
|
+
import { toKebabCase } from "@ingenyus/swarm";
|
|
418
|
+
import { Eta } from "eta";
|
|
419
|
+
import path4 from "path";
|
|
420
|
+
var TemplateUtility = class {
|
|
421
|
+
constructor(fileSystem) {
|
|
422
|
+
this.fileSystem = fileSystem;
|
|
423
|
+
}
|
|
424
|
+
processTemplate(templatePath, replacements) {
|
|
425
|
+
const declarations = Object.keys(replacements).map((key) => `${key}=it.${key}`).join(", ");
|
|
426
|
+
const functionHeader = declarations ? `const ${declarations};` : void 0;
|
|
427
|
+
const templateDir = path4.dirname(templatePath);
|
|
428
|
+
const eta = new Eta({
|
|
429
|
+
autoTrim: false,
|
|
430
|
+
autoEscape: false,
|
|
431
|
+
views: templateDir,
|
|
432
|
+
functionHeader
|
|
433
|
+
});
|
|
434
|
+
const templateName = path4.basename(templatePath).replace(/\.eta$/, "");
|
|
435
|
+
if (this.fileSystem.existsSync(templatePath)) {
|
|
436
|
+
return eta.render(templateName, replacements);
|
|
437
|
+
} else {
|
|
438
|
+
const template = this.fileSystem.readFileSync(templatePath, "utf8");
|
|
439
|
+
return eta.renderString(template, replacements);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Helper method to resolve template paths for concrete generators
|
|
444
|
+
* @param relativePath - The relative path to the template file
|
|
445
|
+
* @param generatorName - The name of the generator (e.g., 'api', 'job')
|
|
446
|
+
* @param currentFileUrl - The import.meta.url from the concrete generator class
|
|
447
|
+
* @returns The full path to the template file
|
|
448
|
+
*/
|
|
449
|
+
resolveTemplatePath(relativePath, generatorName, currentFileUrl) {
|
|
450
|
+
const generatorDirName = toKebabCase(generatorName);
|
|
451
|
+
const currentFilePath = new URL(currentFileUrl).pathname;
|
|
452
|
+
const currentFileDir = path4.dirname(currentFilePath);
|
|
453
|
+
const currentFileName = path4.basename(currentFilePath);
|
|
454
|
+
const isInstalledPackage = currentFileDir.includes("node_modules") && currentFileDir.endsWith("/dist") && currentFileName === "index.js";
|
|
455
|
+
const startDir = isInstalledPackage ? currentFileDir : path4.dirname(path4.dirname(currentFileDir));
|
|
456
|
+
return path4.join(
|
|
457
|
+
startDir,
|
|
458
|
+
"generators",
|
|
459
|
+
generatorDirName,
|
|
460
|
+
"templates",
|
|
461
|
+
relativePath
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// src/plugin.ts
|
|
467
|
+
function createWaspPlugin() {
|
|
468
|
+
return {
|
|
469
|
+
name: PLUGIN_NAME,
|
|
470
|
+
version: getPluginVersion(),
|
|
471
|
+
description: "Wasp Plugin for Swarm",
|
|
472
|
+
swarmVersion: "0.1.0",
|
|
473
|
+
generators: [
|
|
474
|
+
new ActionGenerator(),
|
|
475
|
+
new ApiGenerator(),
|
|
476
|
+
new ApiNamespaceGenerator(),
|
|
477
|
+
new CrudGenerator(),
|
|
478
|
+
new FeatureDirectoryGenerator(),
|
|
479
|
+
new JobGenerator(),
|
|
480
|
+
new QueryGenerator(),
|
|
481
|
+
new RouteGenerator()
|
|
482
|
+
]
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
var _apiPlugin = null;
|
|
486
|
+
function getWaspPlugin() {
|
|
487
|
+
if (!_apiPlugin) {
|
|
488
|
+
_apiPlugin = createWaspPlugin();
|
|
489
|
+
}
|
|
490
|
+
return _apiPlugin;
|
|
491
|
+
}
|
|
492
|
+
var wasp = getWaspPlugin;
|
|
493
|
+
|
|
494
|
+
// src/wasp-config/app.ts
|
|
495
|
+
import fs3 from "fs";
|
|
496
|
+
import path5 from "path";
|
|
497
|
+
import {
|
|
498
|
+
App as WaspApp
|
|
499
|
+
} from "wasp-config";
|
|
500
|
+
var App = class _App extends WaspApp {
|
|
501
|
+
constructor(name, config) {
|
|
502
|
+
super(name, config);
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Static factory method that creates and initializes Swarm with configuration
|
|
506
|
+
* dynamically loaded from feature directories
|
|
507
|
+
*
|
|
508
|
+
* @param name The name of the application
|
|
509
|
+
* @param config The base configuration for the application
|
|
510
|
+
* @returns An initialized Swarm instance
|
|
511
|
+
*/
|
|
512
|
+
static async create(name, config) {
|
|
513
|
+
const app = new _App(name, config);
|
|
514
|
+
await app.configureFeatures();
|
|
515
|
+
return app;
|
|
516
|
+
}
|
|
517
|
+
// Chainable configuration methods
|
|
518
|
+
auth(authConfig) {
|
|
519
|
+
super.auth(authConfig);
|
|
520
|
+
return this;
|
|
521
|
+
}
|
|
522
|
+
client(clientConfig) {
|
|
523
|
+
super.client(clientConfig);
|
|
524
|
+
return this;
|
|
525
|
+
}
|
|
526
|
+
db(dbConfig) {
|
|
527
|
+
super.db(dbConfig);
|
|
528
|
+
return this;
|
|
529
|
+
}
|
|
530
|
+
emailSender(emailSenderConfig) {
|
|
531
|
+
super.emailSender(emailSenderConfig);
|
|
532
|
+
return this;
|
|
533
|
+
}
|
|
534
|
+
job(name, jobConfig) {
|
|
535
|
+
super.job(name, jobConfig);
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
query(name, queryConfig) {
|
|
539
|
+
super.query(name, queryConfig);
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
route(name, routeConfig) {
|
|
543
|
+
super.route(name, routeConfig);
|
|
544
|
+
return this;
|
|
545
|
+
}
|
|
546
|
+
api(name, apiConfig) {
|
|
547
|
+
super.api(name, apiConfig);
|
|
548
|
+
return this;
|
|
549
|
+
}
|
|
550
|
+
apiNamespace(name, apiNamespaceConfig) {
|
|
551
|
+
super.apiNamespace(name, apiNamespaceConfig);
|
|
552
|
+
return this;
|
|
553
|
+
}
|
|
554
|
+
crud(name, crudConfig) {
|
|
555
|
+
super.crud(name, crudConfig);
|
|
556
|
+
return this;
|
|
557
|
+
}
|
|
558
|
+
action(name, actionConfig) {
|
|
559
|
+
super.action(name, actionConfig);
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Helper method to add routes with simplified parameters
|
|
564
|
+
* @param featureName The name of the feature
|
|
565
|
+
* @param name Route name, e.g. "DashboardRoute"
|
|
566
|
+
* @param options Route configuration options
|
|
567
|
+
*/
|
|
568
|
+
addRoute(featureName, name, options) {
|
|
569
|
+
const componentName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
570
|
+
const importPath = this.getFeatureImportPath(
|
|
571
|
+
featureName,
|
|
572
|
+
"client",
|
|
573
|
+
"pages",
|
|
574
|
+
componentName
|
|
575
|
+
);
|
|
576
|
+
const routeConfig = {
|
|
577
|
+
path: options.path,
|
|
578
|
+
to: this.page(componentName, {
|
|
579
|
+
authRequired: options.auth || false,
|
|
580
|
+
component: {
|
|
581
|
+
import: componentName,
|
|
582
|
+
from: `@src/${importPath}`
|
|
583
|
+
}
|
|
584
|
+
})
|
|
585
|
+
};
|
|
586
|
+
super.route(name, routeConfig);
|
|
587
|
+
return this;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Helper method to add API endpoints with simplified parameters
|
|
591
|
+
* @param featureName The name of the feature
|
|
592
|
+
* @param name API endpoint name, e.g. "getTasksApi"
|
|
593
|
+
* @param options API configuration options
|
|
594
|
+
*/
|
|
595
|
+
addApi(featureName, name, options) {
|
|
596
|
+
const importPath = this.getFeatureImportPath(
|
|
597
|
+
featureName,
|
|
598
|
+
"server",
|
|
599
|
+
"apis",
|
|
600
|
+
name
|
|
601
|
+
);
|
|
602
|
+
const middlewareImportPath = this.getFeatureImportPath(
|
|
603
|
+
featureName,
|
|
604
|
+
"server",
|
|
605
|
+
"middleware",
|
|
606
|
+
name
|
|
607
|
+
);
|
|
608
|
+
super.api(name, {
|
|
609
|
+
fn: {
|
|
610
|
+
import: name,
|
|
611
|
+
from: `@src/${importPath}`
|
|
612
|
+
},
|
|
613
|
+
...options.customMiddleware && {
|
|
614
|
+
import: name,
|
|
615
|
+
from: `@src/${middlewareImportPath}`
|
|
616
|
+
},
|
|
617
|
+
entities: options.entities,
|
|
618
|
+
httpRoute: { method: options.method, route: options.route },
|
|
619
|
+
auth: options.auth || false
|
|
620
|
+
});
|
|
621
|
+
return this;
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Helper method to add CRUD operations with simplified parameters
|
|
625
|
+
* @param featureName The name of the feature
|
|
626
|
+
* @param name The CRUD name
|
|
627
|
+
* @param options CRUD configuration options
|
|
628
|
+
*/
|
|
629
|
+
addCrud(featureName, name, options) {
|
|
630
|
+
const processOperationOptions = (operationName, operationOptions) => {
|
|
631
|
+
if (!operationOptions) return void 0;
|
|
632
|
+
const processedOptions = { ...operationOptions };
|
|
633
|
+
if (operationOptions.override) {
|
|
634
|
+
const operationDataType = operationName === "getAll" ? this.getPlural(options.entity) : options.entity;
|
|
635
|
+
const operationComponent = `${operationName}${operationDataType}`;
|
|
636
|
+
const importPath = this.getFeatureImportPath(
|
|
637
|
+
featureName,
|
|
638
|
+
"server",
|
|
639
|
+
"cruds",
|
|
640
|
+
name.charAt(0).toLowerCase() + name.slice(1)
|
|
641
|
+
);
|
|
642
|
+
processedOptions.overrideFn = {
|
|
643
|
+
import: operationComponent,
|
|
644
|
+
from: `@src/${importPath}`
|
|
645
|
+
};
|
|
646
|
+
delete processedOptions.override;
|
|
647
|
+
}
|
|
648
|
+
return processedOptions;
|
|
649
|
+
};
|
|
650
|
+
super.crud(this.getPlural(options.entity), {
|
|
651
|
+
entity: options.entity,
|
|
652
|
+
operations: {
|
|
653
|
+
getAll: processOperationOptions("getAll", options.getAll),
|
|
654
|
+
get: processOperationOptions("get", options.get),
|
|
655
|
+
create: processOperationOptions("create", options.create),
|
|
656
|
+
update: processOperationOptions("update", options.update),
|
|
657
|
+
delete: processOperationOptions("delete", options.delete)
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
return this;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Helper method to add actions with simplified parameters
|
|
664
|
+
* @param featureName The name of the feature
|
|
665
|
+
* @param name The action name
|
|
666
|
+
* @param options Action configuration options
|
|
667
|
+
*/
|
|
668
|
+
addAction(featureName, name, options) {
|
|
669
|
+
const importPath = this.getFeatureImportPath(
|
|
670
|
+
featureName,
|
|
671
|
+
"server",
|
|
672
|
+
"actions",
|
|
673
|
+
name
|
|
674
|
+
);
|
|
675
|
+
const config = this.getOperationConfig(
|
|
676
|
+
name,
|
|
677
|
+
importPath,
|
|
678
|
+
options.entities,
|
|
679
|
+
options.auth
|
|
680
|
+
);
|
|
681
|
+
super.action(name, config);
|
|
682
|
+
return this;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Helper method to add queries with simplified parameters
|
|
686
|
+
* @param featureName The name of the feature
|
|
687
|
+
* @param name The query name
|
|
688
|
+
* @param options Query configuration options
|
|
689
|
+
*/
|
|
690
|
+
addQuery(featureName, name, options) {
|
|
691
|
+
const importPath = this.getFeatureImportPath(
|
|
692
|
+
featureName,
|
|
693
|
+
"server",
|
|
694
|
+
"queries",
|
|
695
|
+
name
|
|
696
|
+
);
|
|
697
|
+
const config = this.getOperationConfig(
|
|
698
|
+
name,
|
|
699
|
+
importPath,
|
|
700
|
+
options.entities,
|
|
701
|
+
options.auth
|
|
702
|
+
);
|
|
703
|
+
super.query(name, config);
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Helper method to add background jobs with simplified parameters
|
|
708
|
+
* @param featureName The name of the feature
|
|
709
|
+
* @param name Job name
|
|
710
|
+
* @param options Job configuration options
|
|
711
|
+
*/
|
|
712
|
+
addJob(featureName, name, options) {
|
|
713
|
+
const importPath = this.getFeatureImportPath(
|
|
714
|
+
featureName,
|
|
715
|
+
"server",
|
|
716
|
+
"jobs",
|
|
717
|
+
name
|
|
718
|
+
);
|
|
719
|
+
super.job(name, {
|
|
720
|
+
executor: "PgBoss",
|
|
721
|
+
perform: {
|
|
722
|
+
fn: {
|
|
723
|
+
import: name,
|
|
724
|
+
from: `@src/${importPath}`
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
entities: options.entities,
|
|
728
|
+
...options.cron && {
|
|
729
|
+
schedule: {
|
|
730
|
+
cron: options.cron,
|
|
731
|
+
args: options.args || {}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
return this;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Helper method to add API namespaces with simplified parameters
|
|
739
|
+
* @param featureName The name of the feature
|
|
740
|
+
* @param name Namespace name
|
|
741
|
+
* @param options API namespace configuration options
|
|
742
|
+
*/
|
|
743
|
+
addApiNamespace(featureName, name, options) {
|
|
744
|
+
const importPath = this.getFeatureImportPath(
|
|
745
|
+
featureName,
|
|
746
|
+
"server",
|
|
747
|
+
"middleware",
|
|
748
|
+
name
|
|
749
|
+
);
|
|
750
|
+
super.apiNamespace(name, {
|
|
751
|
+
path: options.path,
|
|
752
|
+
middlewareConfigFn: {
|
|
753
|
+
import: name,
|
|
754
|
+
from: `@src/${importPath}`
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
return this;
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Calculates the import path for a feature component
|
|
761
|
+
* @param featureName The name of the feature
|
|
762
|
+
* @param type The type of component (client, server, etc.)
|
|
763
|
+
* @param subPath The sub-path within the feature directory
|
|
764
|
+
* @param fileName The name of the file (optional, defaults to featureName)
|
|
765
|
+
* @returns The calculated import path
|
|
766
|
+
*/
|
|
767
|
+
getFeatureImportPath(featureName, type, subPath, fileName) {
|
|
768
|
+
const file = fileName || featureName;
|
|
769
|
+
return `features/${featureName}/${type}/${subPath}/${file}`;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Converts a singular word to its plural form
|
|
773
|
+
* @param word The singular word to pluralize
|
|
774
|
+
* @returns The plural form of the word
|
|
775
|
+
*/
|
|
776
|
+
getPlural(word) {
|
|
777
|
+
if (word.endsWith("y")) {
|
|
778
|
+
return word.slice(0, -1) + "ies";
|
|
779
|
+
} else if (word.endsWith("s") || word.endsWith("sh") || word.endsWith("ch") || word.endsWith("x") || word.endsWith("z")) {
|
|
780
|
+
return word + "es";
|
|
781
|
+
} else {
|
|
782
|
+
return word + "s";
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Configures all feature modules by scanning the features directory
|
|
787
|
+
*/
|
|
788
|
+
async configureFeatures() {
|
|
789
|
+
const featuresDir = path5.join(process.cwd(), "src", "features");
|
|
790
|
+
if (!fs3.existsSync(featuresDir)) {
|
|
791
|
+
console.warn(
|
|
792
|
+
"Features directory not found, skipping feature configuration"
|
|
793
|
+
);
|
|
794
|
+
return this;
|
|
795
|
+
}
|
|
796
|
+
const getAllFeatureFiles = (dir) => {
|
|
797
|
+
let results = [];
|
|
798
|
+
const list = fs3.readdirSync(dir, { withFileTypes: true });
|
|
799
|
+
for (const entry of list) {
|
|
800
|
+
const fullPath = path5.join(dir, entry.name);
|
|
801
|
+
if (entry.isDirectory()) {
|
|
802
|
+
results = results.concat(getAllFeatureFiles(fullPath));
|
|
803
|
+
} else if (entry.isFile() && entry.name.endsWith(".wasp.ts")) {
|
|
804
|
+
results.push(path5.relative(featuresDir, fullPath));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return results;
|
|
808
|
+
};
|
|
809
|
+
const featureFiles = getAllFeatureFiles(featuresDir);
|
|
810
|
+
for (const file of featureFiles) {
|
|
811
|
+
try {
|
|
812
|
+
const featureName = path5.dirname(file);
|
|
813
|
+
const modulePath = path5.join(
|
|
814
|
+
process.cwd(),
|
|
815
|
+
".wasp",
|
|
816
|
+
"src",
|
|
817
|
+
"features",
|
|
818
|
+
file.replace(".ts", ".js")
|
|
819
|
+
);
|
|
820
|
+
const module = await import(modulePath);
|
|
821
|
+
if (module.default) {
|
|
822
|
+
module.default(this, featureName);
|
|
823
|
+
}
|
|
824
|
+
} catch (error) {
|
|
825
|
+
console.error(`Failed to load feature module ${file}:`, error);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return this;
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Helper method to get the configuration for an action or query
|
|
832
|
+
* @param name The operation name
|
|
833
|
+
* @param importPath Import path (excluding `@src/` prefix), e.g. "features/dashboard/server/queries/getTasks"
|
|
834
|
+
* @param entities Comma-separated list of entities (optional, defaults to datatype)
|
|
835
|
+
* @param auth Require authentication (optional)
|
|
836
|
+
*/
|
|
837
|
+
getOperationConfig(name, importPath, entities, auth) {
|
|
838
|
+
return {
|
|
839
|
+
fn: {
|
|
840
|
+
import: name,
|
|
841
|
+
from: `@src/${importPath}`
|
|
842
|
+
},
|
|
843
|
+
entities,
|
|
844
|
+
auth: auth || false
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/generators/base/entity-generator.base.ts
|
|
850
|
+
import {
|
|
851
|
+
hasHelperMethodCall,
|
|
852
|
+
SignaleLogger as SignaleLogger4,
|
|
853
|
+
toCamelCase,
|
|
854
|
+
toKebabCase as toKebabCase2,
|
|
855
|
+
validateFeaturePath as validateFeaturePath3
|
|
856
|
+
} from "@ingenyus/swarm";
|
|
857
|
+
import path8 from "path";
|
|
858
|
+
|
|
859
|
+
// src/generators/feature-directory/feature-directory-generator.ts
|
|
860
|
+
import {
|
|
861
|
+
handleFatalError as handleFatalError2,
|
|
862
|
+
SignaleLogger as SignaleLogger3,
|
|
863
|
+
validateFeaturePath as validateFeaturePath2
|
|
864
|
+
} from "@ingenyus/swarm";
|
|
865
|
+
import path7 from "path";
|
|
866
|
+
|
|
867
|
+
// src/generators/base/wasp-generator.base.ts
|
|
868
|
+
import {
|
|
869
|
+
GeneratorBase,
|
|
870
|
+
SignaleLogger as SignaleLogger2,
|
|
871
|
+
SwarmConfigManager,
|
|
872
|
+
TemplateResolver
|
|
873
|
+
} from "@ingenyus/swarm";
|
|
874
|
+
|
|
875
|
+
// src/generators/config/wasp-config-generator.ts
|
|
876
|
+
import {
|
|
877
|
+
handleFatalError,
|
|
878
|
+
parseHelperMethodDefinition,
|
|
879
|
+
SignaleLogger
|
|
880
|
+
} from "@ingenyus/swarm";
|
|
881
|
+
import path6 from "path";
|
|
882
|
+
var WaspConfigGenerator = class {
|
|
883
|
+
constructor(logger = new SignaleLogger(), fileSystem = realFileSystem) {
|
|
884
|
+
this.logger = logger;
|
|
885
|
+
this.fileSystem = fileSystem;
|
|
886
|
+
this.templateUtility = new TemplateUtility(fileSystem);
|
|
887
|
+
}
|
|
888
|
+
path = path6;
|
|
889
|
+
templateUtility;
|
|
890
|
+
/**
|
|
891
|
+
* Gets the template path for feature config templates.
|
|
892
|
+
* Feature config templates are located in the feature-directory generator's templates directory.
|
|
893
|
+
* @param templateName - The name of the template file (e.g., 'feature.wasp.eta')
|
|
894
|
+
* @returns The full path to the template file
|
|
895
|
+
*/
|
|
896
|
+
getTemplatePath(templateName) {
|
|
897
|
+
return this.templateUtility.resolveTemplatePath(
|
|
898
|
+
templateName,
|
|
899
|
+
"feature-directory",
|
|
900
|
+
import.meta.url
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Generate a TypeScript Wasp config file in a feature directory
|
|
905
|
+
* @param featurePath - The feature directory path
|
|
906
|
+
*/
|
|
907
|
+
generate(featurePath) {
|
|
908
|
+
const featureDir = getFeatureDir(this.fileSystem, featurePath);
|
|
909
|
+
if (!this.fileSystem.existsSync(featureDir)) {
|
|
910
|
+
this.fileSystem.mkdirSync(featureDir, { recursive: true });
|
|
911
|
+
}
|
|
912
|
+
const templatePath = this.getTemplatePath("feature.wasp.eta");
|
|
913
|
+
if (!this.fileSystem.existsSync(templatePath)) {
|
|
914
|
+
this.logger.error(`Template not found: ${templatePath}`);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const configFilePrefix = featurePath.split("/").at(-1);
|
|
918
|
+
const configFilePath = path6.join(featureDir, `${configFilePrefix}.wasp.ts`);
|
|
919
|
+
if (this.fileSystem.existsSync(configFilePath)) {
|
|
920
|
+
this.logger.warn(`Feature config already exists: ${configFilePath}`);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
this.fileSystem.copyFileSync(templatePath, configFilePath);
|
|
924
|
+
this.logger.success(`Generated feature config: ${configFilePath}`);
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Updates or creates a feature configuration file with a pre-built declaration.
|
|
928
|
+
* @param featurePath - The path to the feature
|
|
929
|
+
* @param declaration - The pre-built declaration string to add or update
|
|
930
|
+
* @returns The updated feature configuration file
|
|
931
|
+
*/
|
|
932
|
+
update(featurePath, declaration) {
|
|
933
|
+
const configFilePrefix = featurePath.split("/").at(-1);
|
|
934
|
+
const configDir = getFeatureDir(this.fileSystem, featurePath);
|
|
935
|
+
const configFilePath = path6.join(configDir, `${configFilePrefix}.wasp.ts`);
|
|
936
|
+
if (!this.fileSystem.existsSync(configFilePath)) {
|
|
937
|
+
const templatePath = this.getTemplatePath("feature.wasp.eta");
|
|
938
|
+
if (!this.fileSystem.existsSync(templatePath)) {
|
|
939
|
+
handleFatalError(`Feature config template not found: ${templatePath}`);
|
|
940
|
+
}
|
|
941
|
+
this.fileSystem.copyFileSync(templatePath, configFilePath);
|
|
942
|
+
}
|
|
943
|
+
let content = this.fileSystem.readFileSync(configFilePath, "utf8");
|
|
944
|
+
content = this.normaliseSemicolons(content);
|
|
945
|
+
const parsed = parseHelperMethodDefinition(declaration);
|
|
946
|
+
if (!parsed) {
|
|
947
|
+
handleFatalError(`Could not parse definition: ${declaration}`);
|
|
948
|
+
return content;
|
|
949
|
+
}
|
|
950
|
+
const { methodName } = parsed;
|
|
951
|
+
const hadExistingDefinitions = this.hasExistingDefinitions(
|
|
952
|
+
content,
|
|
953
|
+
methodName
|
|
954
|
+
);
|
|
955
|
+
content = this.removeExistingDefinition(content, declaration);
|
|
956
|
+
const hasExistingDefinitions = this.hasExistingDefinitions(
|
|
957
|
+
content,
|
|
958
|
+
methodName
|
|
959
|
+
);
|
|
960
|
+
const lines = content.split("\n");
|
|
961
|
+
const configureFunctionStart = lines.findIndex(
|
|
962
|
+
(line) => line.trim().startsWith("export default function")
|
|
963
|
+
);
|
|
964
|
+
if (configureFunctionStart === -1) {
|
|
965
|
+
handleFatalError("Could not find configure function in feature config");
|
|
966
|
+
}
|
|
967
|
+
const appLineIndex = lines.findIndex(
|
|
968
|
+
(line, index) => index > configureFunctionStart && line.trim() === "app"
|
|
969
|
+
);
|
|
970
|
+
if (appLineIndex === -1) {
|
|
971
|
+
const insertIndex = configureFunctionStart + 1;
|
|
972
|
+
const itemsToInsert = [" app"];
|
|
973
|
+
const comment = this.getMethodComment(methodName);
|
|
974
|
+
itemsToInsert.push(` ${comment}`);
|
|
975
|
+
itemsToInsert.push(declaration.trimEnd());
|
|
976
|
+
lines.splice(insertIndex, 0, ...itemsToInsert);
|
|
977
|
+
} else {
|
|
978
|
+
const { insertIndex, addComment } = this.findGroupInsertionPoint(
|
|
979
|
+
lines,
|
|
980
|
+
methodName,
|
|
981
|
+
declaration,
|
|
982
|
+
hadExistingDefinitions || hasExistingDefinitions
|
|
983
|
+
);
|
|
984
|
+
const newLines = this.insertWithSpacing(
|
|
985
|
+
lines,
|
|
986
|
+
declaration,
|
|
987
|
+
insertIndex,
|
|
988
|
+
methodName,
|
|
989
|
+
addComment
|
|
990
|
+
);
|
|
991
|
+
const normalisedContent2 = this.normaliseSemicolons(newLines.join("\n"));
|
|
992
|
+
this.fileSystem.writeFileSync(configFilePath, normalisedContent2);
|
|
993
|
+
return configFilePath;
|
|
994
|
+
}
|
|
995
|
+
const normalisedContent = this.normaliseSemicolons(lines.join("\n"));
|
|
996
|
+
this.fileSystem.writeFileSync(configFilePath, normalisedContent);
|
|
997
|
+
return configFilePath;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Determines the insertion index for a method name based on alphabetical ordering
|
|
1001
|
+
* of existing groups in the configuration file.
|
|
1002
|
+
* @param groups - Object containing existing method groups
|
|
1003
|
+
* @param methodName - The method name to find insertion index for
|
|
1004
|
+
* @returns The insertion index for the method name
|
|
1005
|
+
*/
|
|
1006
|
+
getInsertionIndexForMethod(groups, methodName) {
|
|
1007
|
+
const existingMethods = Object.keys(groups).filter(
|
|
1008
|
+
(method) => groups[method].length > 0
|
|
1009
|
+
);
|
|
1010
|
+
const allMethods = [...existingMethods, methodName].sort();
|
|
1011
|
+
return allMethods.indexOf(methodName);
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Gets the comment text for a method type.
|
|
1015
|
+
* @param methodName The method name (e.g., 'addApi')
|
|
1016
|
+
* @returns The comment text for the method type
|
|
1017
|
+
*/
|
|
1018
|
+
getMethodComment(methodName) {
|
|
1019
|
+
const entityName = methodName.startsWith("add") ? methodName.slice(3) : methodName;
|
|
1020
|
+
return `// ${entityName} definitions`;
|
|
1021
|
+
}
|
|
1022
|
+
/**
|
|
1023
|
+
* Finds the correct insertion point for a new configuration item.
|
|
1024
|
+
* @param lines - Array of file lines
|
|
1025
|
+
* @param methodName - The method name (e.g., 'addApi')
|
|
1026
|
+
* @param definition - The definition string to parse for item name
|
|
1027
|
+
* @returns Object with insertion index and whether to add a comment
|
|
1028
|
+
*/
|
|
1029
|
+
findGroupInsertionPoint(lines, methodName, definition, hasExistingDefinitionsOfType) {
|
|
1030
|
+
const appLineIndex = lines.findIndex((line) => line.trim() === "app");
|
|
1031
|
+
if (appLineIndex === -1) {
|
|
1032
|
+
return { insertIndex: appLineIndex + 1, addComment: false };
|
|
1033
|
+
}
|
|
1034
|
+
const methodCalls = [];
|
|
1035
|
+
for (let i = appLineIndex + 1; i < lines.length; i++) {
|
|
1036
|
+
const line = lines[i].trim();
|
|
1037
|
+
if (line.startsWith(".") && line.includes("(")) {
|
|
1038
|
+
let methodCallContent = line;
|
|
1039
|
+
let j = i;
|
|
1040
|
+
let closingParenCount = 0;
|
|
1041
|
+
let foundClosingParen = false;
|
|
1042
|
+
for (let k = 0; k < methodCallContent.length; k++) {
|
|
1043
|
+
if (methodCallContent[k] === "(") closingParenCount++;
|
|
1044
|
+
if (methodCallContent[k] === ")") closingParenCount--;
|
|
1045
|
+
if (closingParenCount === 0 && methodCallContent[k] === ")") {
|
|
1046
|
+
foundClosingParen = true;
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
while (!foundClosingParen && j < lines.length - 1) {
|
|
1051
|
+
j++;
|
|
1052
|
+
methodCallContent += " " + lines[j].trim();
|
|
1053
|
+
for (let k = 0; k < lines[j].length; k++) {
|
|
1054
|
+
if (lines[j][k] === "(") closingParenCount++;
|
|
1055
|
+
if (lines[j][k] === ")") closingParenCount--;
|
|
1056
|
+
if (closingParenCount === 0 && lines[j][k] === ")") {
|
|
1057
|
+
foundClosingParen = true;
|
|
1058
|
+
break;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const match = methodCallContent.match(
|
|
1063
|
+
/\.(\w+)\([^,]+,\s*['"`]([^'"`]+)['"`]/
|
|
1064
|
+
);
|
|
1065
|
+
if (match) {
|
|
1066
|
+
methodCalls.push({
|
|
1067
|
+
lineIndex: i,
|
|
1068
|
+
endLineIndex: j,
|
|
1069
|
+
methodName: match[1],
|
|
1070
|
+
itemName: match[2]
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const groups = {};
|
|
1076
|
+
methodCalls.forEach((call) => {
|
|
1077
|
+
if (!groups[call.methodName]) {
|
|
1078
|
+
groups[call.methodName] = [];
|
|
1079
|
+
}
|
|
1080
|
+
groups[call.methodName].push({
|
|
1081
|
+
lineIndex: call.lineIndex,
|
|
1082
|
+
endLineIndex: call.endLineIndex,
|
|
1083
|
+
itemName: call.itemName
|
|
1084
|
+
});
|
|
1085
|
+
});
|
|
1086
|
+
const targetGroup = groups[methodName] || [];
|
|
1087
|
+
if (targetGroup.length === 0) {
|
|
1088
|
+
const targetGroupIndex = this.getInsertionIndexForMethod(
|
|
1089
|
+
groups,
|
|
1090
|
+
methodName
|
|
1091
|
+
);
|
|
1092
|
+
const existingMethods = Object.keys(groups).filter((method) => groups[method].length > 0).sort();
|
|
1093
|
+
for (let i = targetGroupIndex; i < existingMethods.length; i++) {
|
|
1094
|
+
const groupMethod = existingMethods[i];
|
|
1095
|
+
if (groups[groupMethod] && groups[groupMethod].length > 0) {
|
|
1096
|
+
const firstItem = groups[groupMethod][0];
|
|
1097
|
+
let insertIndex = firstItem.lineIndex;
|
|
1098
|
+
for (let j = firstItem.lineIndex - 1; j > appLineIndex; j--) {
|
|
1099
|
+
const line = lines[j].trim();
|
|
1100
|
+
if (line.startsWith("//") && line.includes("definitions")) {
|
|
1101
|
+
insertIndex = j;
|
|
1102
|
+
break;
|
|
1103
|
+
} else if (line.startsWith(".") || line === "") {
|
|
1104
|
+
continue;
|
|
1105
|
+
} else {
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return { insertIndex, addComment: !hasExistingDefinitionsOfType };
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
for (let i = targetGroupIndex - 1; i >= 0; i--) {
|
|
1113
|
+
const groupMethod = existingMethods[i];
|
|
1114
|
+
if (groups[groupMethod] && groups[groupMethod].length > 0) {
|
|
1115
|
+
const lastItem2 = groups[groupMethod][groups[groupMethod].length - 1];
|
|
1116
|
+
return {
|
|
1117
|
+
insertIndex: lastItem2.endLineIndex + 1,
|
|
1118
|
+
addComment: !hasExistingDefinitionsOfType
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
return {
|
|
1123
|
+
insertIndex: appLineIndex + 1,
|
|
1124
|
+
addComment: !hasExistingDefinitionsOfType
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
const parsed = parseHelperMethodDefinition(definition);
|
|
1128
|
+
if (!parsed) {
|
|
1129
|
+
return { insertIndex: appLineIndex + 1, addComment: false };
|
|
1130
|
+
}
|
|
1131
|
+
const { firstParam: itemName } = parsed;
|
|
1132
|
+
for (let i = 0; i < targetGroup.length; i++) {
|
|
1133
|
+
if (itemName.localeCompare(targetGroup[i].itemName) < 0) {
|
|
1134
|
+
return { insertIndex: targetGroup[i].lineIndex, addComment: false };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const lastItem = targetGroup[targetGroup.length - 1];
|
|
1138
|
+
return { insertIndex: lastItem.endLineIndex + 1, addComment: false };
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Inserts a definition with optional comment header.
|
|
1142
|
+
* @param lines - Array of file lines
|
|
1143
|
+
* @param declaration - The declaration to insert
|
|
1144
|
+
* @param insertIndex - The index where to insert
|
|
1145
|
+
* @param methodName - The method name for comment generation
|
|
1146
|
+
* @param addComment - Whether to add a comment before the declaration
|
|
1147
|
+
* @returns The modified lines array
|
|
1148
|
+
*/
|
|
1149
|
+
insertWithSpacing(lines, declaration, insertIndex, methodName, addComment = false) {
|
|
1150
|
+
const newLines = [...lines];
|
|
1151
|
+
if (addComment) {
|
|
1152
|
+
const comment = this.getMethodComment(methodName);
|
|
1153
|
+
newLines.splice(insertIndex, 0, ` ${comment}`);
|
|
1154
|
+
insertIndex += 1;
|
|
1155
|
+
}
|
|
1156
|
+
newLines.splice(insertIndex, 0, declaration.trimEnd());
|
|
1157
|
+
return newLines;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Checks if there are any existing definitions of a specific type in the content.
|
|
1161
|
+
* @param content - The file content to search
|
|
1162
|
+
* @param methodName - The method name to check for (e.g., 'addJob', 'addApi')
|
|
1163
|
+
* @returns true if there are existing definitions of this type, false otherwise
|
|
1164
|
+
*/
|
|
1165
|
+
hasExistingDefinitions(content, methodName) {
|
|
1166
|
+
const lines = content.split("\n");
|
|
1167
|
+
for (const line of lines) {
|
|
1168
|
+
if (line.trim().startsWith(`.${methodName}(`)) {
|
|
1169
|
+
return true;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Removes an existing definition from the content by finding the helper method call
|
|
1176
|
+
* and removing the entire method call block.
|
|
1177
|
+
* @param content - The file content
|
|
1178
|
+
* @param definition - The new definition to find the existing one from
|
|
1179
|
+
* @returns The content with the existing definition removed
|
|
1180
|
+
*/
|
|
1181
|
+
removeExistingDefinition(content, definition) {
|
|
1182
|
+
const parsed = parseHelperMethodDefinition(definition);
|
|
1183
|
+
if (!parsed) {
|
|
1184
|
+
return content;
|
|
1185
|
+
}
|
|
1186
|
+
const { methodName, firstParam } = parsed;
|
|
1187
|
+
let contentLines = content.split("\n");
|
|
1188
|
+
let openingLineIndex = -1;
|
|
1189
|
+
for (let i = 0; i < contentLines.length; i++) {
|
|
1190
|
+
const line = contentLines[i];
|
|
1191
|
+
if (line.trim().startsWith(`.${methodName}(`)) {
|
|
1192
|
+
if (firstParam && line.includes(firstParam)) {
|
|
1193
|
+
openingLineIndex = i;
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (openingLineIndex === -1) {
|
|
1199
|
+
return content;
|
|
1200
|
+
}
|
|
1201
|
+
let closingLineIndex = -1;
|
|
1202
|
+
let parenCount = 0;
|
|
1203
|
+
let braceCount = 0;
|
|
1204
|
+
let foundOpening = false;
|
|
1205
|
+
for (let i = openingLineIndex; i < contentLines.length; i++) {
|
|
1206
|
+
const line = contentLines[i];
|
|
1207
|
+
for (const char of line) {
|
|
1208
|
+
if (char === "(") {
|
|
1209
|
+
parenCount++;
|
|
1210
|
+
foundOpening = true;
|
|
1211
|
+
} else if (char === ")") {
|
|
1212
|
+
parenCount--;
|
|
1213
|
+
if (foundOpening && parenCount === 0 && braceCount === 0) {
|
|
1214
|
+
closingLineIndex = i;
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
} else if (char === "{") {
|
|
1218
|
+
braceCount++;
|
|
1219
|
+
} else if (char === "}") {
|
|
1220
|
+
braceCount--;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (closingLineIndex !== -1) {
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (closingLineIndex === -1) {
|
|
1228
|
+
this.logger.warn(
|
|
1229
|
+
"Could not find closing parenthesis for existing definition"
|
|
1230
|
+
);
|
|
1231
|
+
return content;
|
|
1232
|
+
}
|
|
1233
|
+
contentLines.splice(
|
|
1234
|
+
openingLineIndex,
|
|
1235
|
+
closingLineIndex - openingLineIndex + 1
|
|
1236
|
+
);
|
|
1237
|
+
return contentLines.join("\n");
|
|
1238
|
+
}
|
|
1239
|
+
/**
|
|
1240
|
+
* Adds a definition to the content by finding the appropriate place to insert it.
|
|
1241
|
+
* @param content - The current file content
|
|
1242
|
+
* @param definition - The definition to add
|
|
1243
|
+
* @returns The updated content with the new definition
|
|
1244
|
+
*/
|
|
1245
|
+
addDefinitionToContent(content, definition) {
|
|
1246
|
+
const lines = content.split("\n");
|
|
1247
|
+
const lastLineIndex = lines.length - 1;
|
|
1248
|
+
let insertIndex = lastLineIndex;
|
|
1249
|
+
for (let i = lastLineIndex; i >= 0; i--) {
|
|
1250
|
+
const line = lines[i].trim();
|
|
1251
|
+
if (line && !line.startsWith("}")) {
|
|
1252
|
+
insertIndex = i;
|
|
1253
|
+
break;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
lines.splice(insertIndex + 1, 0, ` ${definition}`);
|
|
1257
|
+
return lines.join("\n");
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Normalises semicolons in the config file by removing them from method chain calls
|
|
1261
|
+
* while preserving them in other contexts (imports, declarations, etc.).
|
|
1262
|
+
* @param content - The file content to normalise
|
|
1263
|
+
* @returns The normalised content
|
|
1264
|
+
*/
|
|
1265
|
+
normaliseSemicolons(content) {
|
|
1266
|
+
const lines = content.split("\n");
|
|
1267
|
+
const configureFunctionStart = lines.findIndex(
|
|
1268
|
+
(line) => line.trim().startsWith("export default function")
|
|
1269
|
+
);
|
|
1270
|
+
if (configureFunctionStart === -1) {
|
|
1271
|
+
return content;
|
|
1272
|
+
}
|
|
1273
|
+
const appLineIndex = lines.findIndex(
|
|
1274
|
+
(line, index) => index > configureFunctionStart && line.trim().startsWith("app")
|
|
1275
|
+
);
|
|
1276
|
+
if (appLineIndex === -1) {
|
|
1277
|
+
return content;
|
|
1278
|
+
}
|
|
1279
|
+
let braceCount = 0;
|
|
1280
|
+
let functionEndIndex = lines.length - 1;
|
|
1281
|
+
for (let i = configureFunctionStart; i < lines.length; i++) {
|
|
1282
|
+
const line = lines[i];
|
|
1283
|
+
for (const char of line) {
|
|
1284
|
+
if (char === "{") braceCount++;
|
|
1285
|
+
if (char === "}") {
|
|
1286
|
+
braceCount--;
|
|
1287
|
+
if (braceCount === 0) {
|
|
1288
|
+
functionEndIndex = i;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (braceCount === 0 && i > configureFunctionStart) {
|
|
1294
|
+
break;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
let lastMethodCallIndex = -1;
|
|
1298
|
+
for (let i = appLineIndex + 1; i < functionEndIndex; i++) {
|
|
1299
|
+
const line = lines[i];
|
|
1300
|
+
const trimmed = line.trim();
|
|
1301
|
+
if ((trimmed.endsWith(")") || trimmed.endsWith(");")) && !trimmed.startsWith("//")) {
|
|
1302
|
+
lines[i] = line.replace(/;\s*$/, "");
|
|
1303
|
+
lastMethodCallIndex = i;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (lastMethodCallIndex !== -1 && !lines[lastMethodCallIndex].trim().endsWith(";")) {
|
|
1307
|
+
lines[lastMethodCallIndex] = lines[lastMethodCallIndex] + ";";
|
|
1308
|
+
}
|
|
1309
|
+
return lines.join("\n");
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
// src/generators/base/wasp-generator.base.ts
|
|
1314
|
+
var WaspGeneratorBase = class extends GeneratorBase {
|
|
1315
|
+
constructor(fileSystem = realFileSystem, logger = new SignaleLogger2()) {
|
|
1316
|
+
super(fileSystem, logger);
|
|
1317
|
+
this.fileSystem = fileSystem;
|
|
1318
|
+
this.logger = logger;
|
|
1319
|
+
this.configGenerator = new WaspConfigGenerator(logger, fileSystem);
|
|
1320
|
+
this.templateUtility = new TemplateUtility(fileSystem);
|
|
1321
|
+
this.templateResolver = new TemplateResolver(fileSystem);
|
|
1322
|
+
}
|
|
1323
|
+
configGenerator;
|
|
1324
|
+
templateUtility;
|
|
1325
|
+
templateResolver;
|
|
1326
|
+
swarmConfig;
|
|
1327
|
+
configLoaded = false;
|
|
1328
|
+
// Plugin name from swarm.config.json
|
|
1329
|
+
pluginName = PLUGIN_NAME;
|
|
1330
|
+
async loadSwarmConfig() {
|
|
1331
|
+
if (this.configLoaded) return;
|
|
1332
|
+
const configManager = new SwarmConfigManager();
|
|
1333
|
+
this.swarmConfig = await configManager.loadConfig();
|
|
1334
|
+
this.configLoaded = true;
|
|
1335
|
+
}
|
|
1336
|
+
async getCustomTemplateDir() {
|
|
1337
|
+
await this.loadSwarmConfig();
|
|
1338
|
+
return this.swarmConfig?.templateDirectory;
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Resolves template path with override support
|
|
1342
|
+
*/
|
|
1343
|
+
async getTemplatePath(templateName) {
|
|
1344
|
+
const defaultPath = this.getDefaultTemplatePath(templateName);
|
|
1345
|
+
const customPath = await this.getCustomTemplateDir();
|
|
1346
|
+
if (!customPath) {
|
|
1347
|
+
return defaultPath;
|
|
1348
|
+
}
|
|
1349
|
+
const { path: resolvedPath, isCustom } = this.templateResolver.resolveTemplatePath(
|
|
1350
|
+
this.pluginName,
|
|
1351
|
+
this.name,
|
|
1352
|
+
templateName,
|
|
1353
|
+
defaultPath,
|
|
1354
|
+
customPath
|
|
1355
|
+
);
|
|
1356
|
+
if (isCustom) {
|
|
1357
|
+
this.logger.info(`Using custom template: ${resolvedPath}`);
|
|
1358
|
+
}
|
|
1359
|
+
return resolvedPath;
|
|
1360
|
+
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Processes a template and writes the result to a file
|
|
1363
|
+
*/
|
|
1364
|
+
async renderTemplateToFile(templateName, replacements, outputPath, readableFileType, force) {
|
|
1365
|
+
const templatePath = await this.getTemplatePath(templateName);
|
|
1366
|
+
const fileExists = this.checkFileExists(
|
|
1367
|
+
outputPath,
|
|
1368
|
+
force,
|
|
1369
|
+
readableFileType
|
|
1370
|
+
);
|
|
1371
|
+
const content = this.templateUtility.processTemplate(
|
|
1372
|
+
templatePath,
|
|
1373
|
+
replacements
|
|
1374
|
+
);
|
|
1375
|
+
this.writeFile(outputPath, content, readableFileType, fileExists);
|
|
1376
|
+
return fileExists;
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Generic existence check with force flag handling
|
|
1380
|
+
* Consolidates the pattern used in both file and config checks
|
|
1381
|
+
*/
|
|
1382
|
+
checkExistence(exists, itemDescription, force, errorMessage) {
|
|
1383
|
+
if (exists && !force) {
|
|
1384
|
+
this.logger.error(`${itemDescription}. Use --force to overwrite`);
|
|
1385
|
+
throw new Error(errorMessage || itemDescription);
|
|
1386
|
+
}
|
|
1387
|
+
return exists;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Checks if a file exists and handles force flag logic
|
|
1391
|
+
*/
|
|
1392
|
+
checkFileExists(filePath, force, fileType) {
|
|
1393
|
+
const fileExists = this.fileSystem.existsSync(filePath);
|
|
1394
|
+
return this.checkExistence(
|
|
1395
|
+
fileExists,
|
|
1396
|
+
`${fileType} already exists: ${filePath}`,
|
|
1397
|
+
force,
|
|
1398
|
+
`${fileType} already exists`
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Safely writes a file with proper error handling and logging
|
|
1403
|
+
*/
|
|
1404
|
+
writeFile(filePath, content, fileType, fileExists) {
|
|
1405
|
+
this.fileSystem.writeFileSync(filePath, content);
|
|
1406
|
+
this.logger.success(
|
|
1407
|
+
`${fileExists ? "Overwrote" : "Generated"} ${fileType}: ${filePath}`
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// src/generators/feature-directory/schema.ts
|
|
1413
|
+
import { z as z2 } from "zod";
|
|
1414
|
+
var schema = z2.object({
|
|
1415
|
+
path: commonSchemas.path
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
// src/generators/feature-directory/feature-directory-generator.ts
|
|
1419
|
+
var FeatureDirectoryGenerator = class extends WaspGeneratorBase {
|
|
1420
|
+
constructor(logger = new SignaleLogger3(), fileSystem = realFileSystem) {
|
|
1421
|
+
super(fileSystem, logger);
|
|
1422
|
+
this.logger = logger;
|
|
1423
|
+
this.fileSystem = fileSystem;
|
|
1424
|
+
this.name = "feature-directory";
|
|
1425
|
+
this.description = "Generate feature directory structure";
|
|
1426
|
+
this.schema = schema;
|
|
1427
|
+
}
|
|
1428
|
+
name;
|
|
1429
|
+
description;
|
|
1430
|
+
schema;
|
|
1431
|
+
getDefaultTemplatePath(templateName) {
|
|
1432
|
+
return this.templateUtility.resolveTemplatePath(
|
|
1433
|
+
templateName,
|
|
1434
|
+
this.name,
|
|
1435
|
+
import.meta.url
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
/**
|
|
1439
|
+
* Generate feature directory structure (main entry point)
|
|
1440
|
+
* @param featurePath - The path to the feature
|
|
1441
|
+
*/
|
|
1442
|
+
async generate(flags) {
|
|
1443
|
+
const { path: featurePath } = flags;
|
|
1444
|
+
console.log("generate feature directory:", featurePath);
|
|
1445
|
+
const segments = validateFeaturePath2(featurePath);
|
|
1446
|
+
const normalisedPath = normaliseFeaturePath(featurePath);
|
|
1447
|
+
const sourceRoot = path7.join(findWaspRoot(this.fileSystem), "src");
|
|
1448
|
+
if (segments.length > 1) {
|
|
1449
|
+
const parentPath = segments.slice(0, -1).join("/");
|
|
1450
|
+
const parentNormalisedPath = normaliseFeaturePath(parentPath);
|
|
1451
|
+
const parentFeatureDir = path7.join(sourceRoot, parentNormalisedPath);
|
|
1452
|
+
if (!this.fileSystem.existsSync(parentFeatureDir)) {
|
|
1453
|
+
handleFatalError2(
|
|
1454
|
+
`Parent feature '${parentPath}' does not exist. Please create it first.`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const featureDir = path7.join(sourceRoot, normalisedPath);
|
|
1459
|
+
this.fileSystem.mkdirSync(featureDir, { recursive: true });
|
|
1460
|
+
this.configGenerator.generate(normalisedPath);
|
|
1461
|
+
this.logger.success(`Generated feature: ${normalisedPath}`);
|
|
1462
|
+
}
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// src/generators/base/entity-generator.base.ts
|
|
1466
|
+
var EntityGeneratorBase = class extends WaspGeneratorBase {
|
|
1467
|
+
constructor(logger = new SignaleLogger4(), fileSystem = realFileSystem, featureDirectoryGenerator = new FeatureDirectoryGenerator(
|
|
1468
|
+
logger,
|
|
1469
|
+
fileSystem
|
|
1470
|
+
)) {
|
|
1471
|
+
super(fileSystem, logger);
|
|
1472
|
+
this.logger = logger;
|
|
1473
|
+
this.fileSystem = fileSystem;
|
|
1474
|
+
this.featureDirectoryGenerator = featureDirectoryGenerator;
|
|
1475
|
+
this.featureDirectoryGenerator = featureDirectoryGenerator;
|
|
1476
|
+
}
|
|
1477
|
+
getDefaultTemplatePath(templateName) {
|
|
1478
|
+
return this.templateUtility.resolveTemplatePath(
|
|
1479
|
+
templateName,
|
|
1480
|
+
this.name,
|
|
1481
|
+
import.meta.url
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
get name() {
|
|
1485
|
+
return toKebabCase2(this.entityType.toString());
|
|
1486
|
+
}
|
|
1487
|
+
/**
|
|
1488
|
+
* Validates that the feature config file exists in the target or ancestor directories
|
|
1489
|
+
*/
|
|
1490
|
+
validateFeatureConfig(featurePath) {
|
|
1491
|
+
const normalisedPath = normaliseFeaturePath(featurePath);
|
|
1492
|
+
const segments = normalisedPath.split("/");
|
|
1493
|
+
for (let i = segments.length; i > 0; i--) {
|
|
1494
|
+
const pathSegments = segments.slice(0, i);
|
|
1495
|
+
const currentPath = pathSegments.join("/");
|
|
1496
|
+
const featureName = pathSegments[pathSegments.length - 1];
|
|
1497
|
+
const featureDir = getFeatureDir(this.fileSystem, currentPath);
|
|
1498
|
+
const configPath = path8.join(featureDir, `${featureName}.wasp.ts`);
|
|
1499
|
+
if (this.fileSystem.existsSync(configPath)) {
|
|
1500
|
+
return configPath;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
this.logger.error(
|
|
1504
|
+
`Feature config file not found in '${normalisedPath}' or any ancestor directories`
|
|
1505
|
+
);
|
|
1506
|
+
this.logger.error(
|
|
1507
|
+
`Expected to find a .wasp.ts config file in one of the feature directories`
|
|
1508
|
+
);
|
|
1509
|
+
throw new Error("Feature config file not found");
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Checks if a config item already exists in the feature config
|
|
1513
|
+
*/
|
|
1514
|
+
checkConfigExists(configPath, methodName, itemName, force) {
|
|
1515
|
+
const configContent = this.fileSystem.readFileSync(configPath, "utf8");
|
|
1516
|
+
const configExists = hasHelperMethodCall(
|
|
1517
|
+
configContent,
|
|
1518
|
+
methodName,
|
|
1519
|
+
itemName
|
|
1520
|
+
);
|
|
1521
|
+
return this.checkExistence(
|
|
1522
|
+
configExists,
|
|
1523
|
+
`${methodName} config already exists in ${configPath}`,
|
|
1524
|
+
force,
|
|
1525
|
+
`${methodName} config already exists`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Updates the feature config with a new definition
|
|
1530
|
+
*/
|
|
1531
|
+
updateFeatureConfig(featurePath, definition, configPath, configExists, methodName) {
|
|
1532
|
+
this.configGenerator.update(featurePath, definition);
|
|
1533
|
+
this.logger.success(
|
|
1534
|
+
`${configExists ? "Updated" : "Added"} ${methodName} config in: ${configPath}`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Consolidated helper for updating config files with existence check
|
|
1539
|
+
* This replaces the duplicated updateConfigFile pattern in concrete generators
|
|
1540
|
+
*/
|
|
1541
|
+
updateConfigWithCheck(configPath, methodName, entityName, definition, featurePath, force) {
|
|
1542
|
+
const configExists = this.checkConfigExists(
|
|
1543
|
+
configPath,
|
|
1544
|
+
methodName,
|
|
1545
|
+
entityName,
|
|
1546
|
+
force
|
|
1547
|
+
);
|
|
1548
|
+
if (!configExists || force) {
|
|
1549
|
+
this.updateFeatureConfig(
|
|
1550
|
+
featurePath,
|
|
1551
|
+
definition,
|
|
1552
|
+
configPath,
|
|
1553
|
+
configExists,
|
|
1554
|
+
methodName
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Gets the appropriate directory for a feature based on its path.
|
|
1560
|
+
* @param fileSystem - The filesystem abstraction
|
|
1561
|
+
* @param featurePath - The full feature path
|
|
1562
|
+
* @param type - The type of file being generated
|
|
1563
|
+
* @returns The target directory and import path
|
|
1564
|
+
*/
|
|
1565
|
+
getFeatureTargetDir(fileSystem, featurePath, type) {
|
|
1566
|
+
validateFeaturePath3(featurePath);
|
|
1567
|
+
const normalisedPath = normaliseFeaturePath(featurePath);
|
|
1568
|
+
const featureDir = getFeatureDir(fileSystem, normalisedPath);
|
|
1569
|
+
const typeKey = type.toLowerCase();
|
|
1570
|
+
const typeDirectory = TYPE_DIRECTORIES[typeKey];
|
|
1571
|
+
const targetDirectory = path8.join(featureDir, typeDirectory);
|
|
1572
|
+
const importDirectory = `@src/${normalisedPath}/${typeDirectory}`;
|
|
1573
|
+
return { targetDirectory, importDirectory };
|
|
1574
|
+
}
|
|
1575
|
+
/**
|
|
1576
|
+
* Ensures a target directory exists and returns its path
|
|
1577
|
+
*/
|
|
1578
|
+
ensureTargetDirectory(featurePath, type) {
|
|
1579
|
+
const { targetDirectory, importDirectory } = this.getFeatureTargetDir(
|
|
1580
|
+
this.fileSystem,
|
|
1581
|
+
featurePath,
|
|
1582
|
+
type
|
|
1583
|
+
);
|
|
1584
|
+
ensureDirectoryExists(this.fileSystem, targetDirectory);
|
|
1585
|
+
return { targetDirectory, importDirectory };
|
|
1586
|
+
}
|
|
1587
|
+
/**
|
|
1588
|
+
* Generate middleware file for API or API namespace
|
|
1589
|
+
*/
|
|
1590
|
+
async generateMiddlewareFile(targetFile, name, force) {
|
|
1591
|
+
const replacements = {
|
|
1592
|
+
name,
|
|
1593
|
+
middlewareType: toCamelCase(this.entityType || "")
|
|
1594
|
+
};
|
|
1595
|
+
await this.renderTemplateToFile(
|
|
1596
|
+
"middleware.eta",
|
|
1597
|
+
replacements,
|
|
1598
|
+
targetFile,
|
|
1599
|
+
"Middleware file",
|
|
1600
|
+
force
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
// src/generators/base/operation-generator.base.ts
|
|
1606
|
+
import {
|
|
1607
|
+
capitalise,
|
|
1608
|
+
getPlural,
|
|
1609
|
+
handleFatalError as handleFatalError3,
|
|
1610
|
+
toPascalCase as toPascalCase2
|
|
1611
|
+
} from "@ingenyus/swarm";
|
|
1612
|
+
var OperationGeneratorBase = class extends EntityGeneratorBase {
|
|
1613
|
+
/**
|
|
1614
|
+
* Gets the operation name based on operation type and model name.
|
|
1615
|
+
*/
|
|
1616
|
+
getOperationName(operation, modelName, customName) {
|
|
1617
|
+
if (customName) {
|
|
1618
|
+
return customName;
|
|
1619
|
+
}
|
|
1620
|
+
switch (operation) {
|
|
1621
|
+
case OPERATIONS.GETALL:
|
|
1622
|
+
return `getAll${getPlural(modelName)}`;
|
|
1623
|
+
case OPERATIONS.GETFILTERED:
|
|
1624
|
+
return `getFiltered${getPlural(modelName)}`;
|
|
1625
|
+
default:
|
|
1626
|
+
return `${operation}${modelName}`;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Gets the template path for operation templates.
|
|
1631
|
+
* This method resolves operation templates to the operation generator's directory
|
|
1632
|
+
* instead of the current generator's directory.
|
|
1633
|
+
*/
|
|
1634
|
+
getOperationTemplatePath(templateName) {
|
|
1635
|
+
return this.templateUtility.resolveTemplatePath(
|
|
1636
|
+
templateName,
|
|
1637
|
+
"operation",
|
|
1638
|
+
import.meta.url
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Gets the TypeScript type name for an operation.
|
|
1643
|
+
*/
|
|
1644
|
+
getOperationTypeName(operation, modelName) {
|
|
1645
|
+
return toPascalCase2(this.getOperationName(operation, modelName));
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Generates import statements for an operation.
|
|
1649
|
+
*/
|
|
1650
|
+
generateImports(model, modelName, operation) {
|
|
1651
|
+
const imports = [];
|
|
1652
|
+
if (operation !== OPERATIONS.GETALL) {
|
|
1653
|
+
if (needsPrismaImport(model)) {
|
|
1654
|
+
imports.push('import { Prisma } from "@prisma/client";');
|
|
1655
|
+
}
|
|
1656
|
+
imports.push(`import { ${modelName} } from "wasp/entities";`);
|
|
1657
|
+
}
|
|
1658
|
+
imports.push('import { HttpError } from "wasp/server";');
|
|
1659
|
+
imports.push(
|
|
1660
|
+
`import type { ${this.getOperationTypeName(
|
|
1661
|
+
operation,
|
|
1662
|
+
modelName
|
|
1663
|
+
)} } from "wasp/server/operations";`
|
|
1664
|
+
);
|
|
1665
|
+
return imports.join("\n");
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Gets the operation type ("query" or "action") for a given operation.
|
|
1669
|
+
*/
|
|
1670
|
+
getOperationType(operation) {
|
|
1671
|
+
return operation === OPERATIONS.GETALL || operation === OPERATIONS.GET || operation === OPERATIONS.GETFILTERED ? "query" : "action";
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Generates the operation components needed for file and config generation.
|
|
1675
|
+
*/
|
|
1676
|
+
async generateOperationComponents(modelName, operation, auth = false, entities = [modelName], isCrudOverride = false, crudName = null, customName) {
|
|
1677
|
+
const model = await getEntityMetadata(modelName);
|
|
1678
|
+
const operationType = this.getOperationType(operation);
|
|
1679
|
+
const operationName = this.getOperationName(
|
|
1680
|
+
operation,
|
|
1681
|
+
modelName,
|
|
1682
|
+
customName
|
|
1683
|
+
);
|
|
1684
|
+
const operationCode = await this.generateOperationCode(
|
|
1685
|
+
model,
|
|
1686
|
+
operation,
|
|
1687
|
+
auth,
|
|
1688
|
+
isCrudOverride,
|
|
1689
|
+
crudName
|
|
1690
|
+
);
|
|
1691
|
+
const configEntry = {
|
|
1692
|
+
operationName,
|
|
1693
|
+
entities,
|
|
1694
|
+
authRequired: auth
|
|
1695
|
+
};
|
|
1696
|
+
return {
|
|
1697
|
+
operationCode,
|
|
1698
|
+
configEntry,
|
|
1699
|
+
operationType,
|
|
1700
|
+
operationName
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Generates the code for an operation.
|
|
1705
|
+
*/
|
|
1706
|
+
async generateOperationCode(model, operation, auth = false, isCrudOverride = false, crudName = null) {
|
|
1707
|
+
const operationType = this.getOperationType(operation);
|
|
1708
|
+
const templatePath = this.getOperationTemplatePath(`${operation}.eta`);
|
|
1709
|
+
const allFieldNames = model.fields.map((f) => f.name);
|
|
1710
|
+
const idFields = getIdFields(model);
|
|
1711
|
+
const requiredFields = getRequiredFields(model);
|
|
1712
|
+
const optionalFields = getOptionalFields(model);
|
|
1713
|
+
const jsonFields = getJsonFields(model);
|
|
1714
|
+
const pluralModelName = getPlural(model.name);
|
|
1715
|
+
const pluralModelNameLower = pluralModelName.toLowerCase();
|
|
1716
|
+
const modelNameLower = model.name.toLowerCase();
|
|
1717
|
+
const operationName = this.getOperationName(operation, model.name);
|
|
1718
|
+
const imports = isCrudOverride ? "" : this.generateImports(model, model.name, operation);
|
|
1719
|
+
const jsonTypeHandling = generateJsonTypeHandling(jsonFields);
|
|
1720
|
+
let typeParams = "";
|
|
1721
|
+
switch (operation) {
|
|
1722
|
+
case "create": {
|
|
1723
|
+
const pickRequired = generatePickType(
|
|
1724
|
+
model.name,
|
|
1725
|
+
requiredFields,
|
|
1726
|
+
allFieldNames
|
|
1727
|
+
);
|
|
1728
|
+
const partialOptional = generatePartialType(
|
|
1729
|
+
generatePickType(model.name, optionalFields, allFieldNames)
|
|
1730
|
+
);
|
|
1731
|
+
typeParams = `<${generateIntersectionType(pickRequired, partialOptional)}>`;
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
case "update": {
|
|
1735
|
+
const pickId = generatePickType(model.name, idFields, allFieldNames);
|
|
1736
|
+
const omitId = generateOmitType(model.name, idFields, allFieldNames);
|
|
1737
|
+
const partialRest = generatePartialType(omitId);
|
|
1738
|
+
typeParams = `<${generateIntersectionType(pickId, partialRest)}>`;
|
|
1739
|
+
break;
|
|
1740
|
+
}
|
|
1741
|
+
case "delete":
|
|
1742
|
+
case "get":
|
|
1743
|
+
typeParams = `<${generatePickType(model.name, idFields, allFieldNames)}>`;
|
|
1744
|
+
break;
|
|
1745
|
+
case "getAll":
|
|
1746
|
+
typeParams = `<void>`;
|
|
1747
|
+
break;
|
|
1748
|
+
case "getFiltered":
|
|
1749
|
+
typeParams = `<${generatePartialType(model.name)}>`;
|
|
1750
|
+
break;
|
|
1751
|
+
}
|
|
1752
|
+
const authCheck = auth ? ` if (!context.user) {
|
|
1753
|
+
throw new HttpError(401);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
` : "";
|
|
1757
|
+
let typeAnnotation = "";
|
|
1758
|
+
let satisfiesType = "";
|
|
1759
|
+
if (isCrudOverride && crudName) {
|
|
1760
|
+
const opCap = capitalise(operation);
|
|
1761
|
+
if (operationType === "action") {
|
|
1762
|
+
typeAnnotation = `: ${crudName}.${opCap}Action${typeParams}`;
|
|
1763
|
+
} else {
|
|
1764
|
+
typeAnnotation = "";
|
|
1765
|
+
}
|
|
1766
|
+
if (operationType === "query") {
|
|
1767
|
+
satisfiesType = `satisfies ${crudName}.${opCap}Query${typeParams}`;
|
|
1768
|
+
} else {
|
|
1769
|
+
satisfiesType = "";
|
|
1770
|
+
}
|
|
1771
|
+
} else {
|
|
1772
|
+
if (operationType === "action") {
|
|
1773
|
+
typeAnnotation = `: ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
|
|
1774
|
+
} else {
|
|
1775
|
+
typeAnnotation = "";
|
|
1776
|
+
}
|
|
1777
|
+
if (operationType === "query") {
|
|
1778
|
+
satisfiesType = `satisfies ${this.getOperationTypeName(operation, model.name)}${typeParams}`;
|
|
1779
|
+
} else {
|
|
1780
|
+
satisfiesType = "";
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
const isCompositeKey = idFields.length > 1;
|
|
1784
|
+
const compositeKeyName = isCompositeKey ? idFields.join("_") : "";
|
|
1785
|
+
const idFieldParams = isCompositeKey ? idFields.join(", ") : idFields[0];
|
|
1786
|
+
const whereClause = isCompositeKey ? `${compositeKeyName}: { ${idFields.map((f) => `${f}`).join(", ")} }` : idFields[0];
|
|
1787
|
+
const replacements = {
|
|
1788
|
+
operationName,
|
|
1789
|
+
modelName: model.name,
|
|
1790
|
+
authCheck,
|
|
1791
|
+
imports,
|
|
1792
|
+
idField: idFields[0],
|
|
1793
|
+
idFieldParams,
|
|
1794
|
+
whereClause,
|
|
1795
|
+
isCompositeKey: String(isCompositeKey),
|
|
1796
|
+
compositeKeyName,
|
|
1797
|
+
jsonTypeHandling,
|
|
1798
|
+
typeAnnotation,
|
|
1799
|
+
satisfiesType,
|
|
1800
|
+
modelNameLower,
|
|
1801
|
+
pluralModelNameLower
|
|
1802
|
+
};
|
|
1803
|
+
return this.templateUtility.processTemplate(templatePath, replacements);
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Generates an operation file for a given operation.
|
|
1807
|
+
*/
|
|
1808
|
+
generateOperationFile(operationsDir, operationName, operationCode, force = false) {
|
|
1809
|
+
const operationFile = `${operationsDir}/${operationName}.ts`;
|
|
1810
|
+
const fileExists = this.checkFileExists(
|
|
1811
|
+
operationFile,
|
|
1812
|
+
force,
|
|
1813
|
+
"Operation file"
|
|
1814
|
+
);
|
|
1815
|
+
this.writeFile(operationFile, operationCode, "operation file", fileExists);
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Copies a directory of operation templates to the target feature directory.
|
|
1819
|
+
* @param templateDir - The source template directory
|
|
1820
|
+
* @param targetDir - The target feature directory
|
|
1821
|
+
*/
|
|
1822
|
+
copyOperationTemplates(templateDir, targetDir) {
|
|
1823
|
+
copyDirectory(this.fileSystem, templateDir, targetDir);
|
|
1824
|
+
this.logger.debug(
|
|
1825
|
+
`Copied operation templates from ${templateDir} to ${targetDir}`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* Generates an operation definition for the feature configuration.
|
|
1830
|
+
*/
|
|
1831
|
+
getDefinition(operationName, featurePath, entities, operationType, importPath, auth = false) {
|
|
1832
|
+
if (!OPERATION_TYPES.includes(operationType)) {
|
|
1833
|
+
handleFatalError3(`Unknown operation type: ${operationType}`);
|
|
1834
|
+
}
|
|
1835
|
+
const directory = TYPE_DIRECTORIES[operationType];
|
|
1836
|
+
const featureDir = getFeatureImportPath(featurePath);
|
|
1837
|
+
const templatePath = this.templateUtility.resolveTemplatePath(
|
|
1838
|
+
"operation.eta",
|
|
1839
|
+
"config",
|
|
1840
|
+
import.meta.url
|
|
1841
|
+
);
|
|
1842
|
+
return this.templateUtility.processTemplate(templatePath, {
|
|
1843
|
+
operationType: capitalise(operationType),
|
|
1844
|
+
operationName,
|
|
1845
|
+
featureDir,
|
|
1846
|
+
directory,
|
|
1847
|
+
entities: entities.map((e) => `"${e}"`).join(", "),
|
|
1848
|
+
importPath,
|
|
1849
|
+
auth: String(auth)
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
|
|
1854
|
+
// src/generators/action/schema.ts
|
|
1855
|
+
import { extend as extend2 } from "@ingenyus/swarm";
|
|
1856
|
+
import { z as z3 } from "zod";
|
|
1857
|
+
var validActions = Object.values(ACTION_OPERATIONS);
|
|
1858
|
+
var actionSchema = extend2(
|
|
1859
|
+
z3.string().min(1, "Action type is required").refine(getTypedValueValidator(validActions), {
|
|
1860
|
+
message: `Invalid action. Must be one of: ${validActions.join(", ")}`
|
|
1861
|
+
}).transform(getTypedValueTransformer(validActions)),
|
|
1862
|
+
{
|
|
1863
|
+
description: "The action operation to generate",
|
|
1864
|
+
friendlyName: "Action Operation",
|
|
1865
|
+
shortName: "o",
|
|
1866
|
+
examples: validActions,
|
|
1867
|
+
helpText: "Available actions: create, update, delete"
|
|
1868
|
+
}
|
|
1869
|
+
);
|
|
1870
|
+
var dataTypeSchema = extend2(z3.string().min(1, "Data type is required"), {
|
|
1871
|
+
description: "The data type/model name for this action",
|
|
1872
|
+
friendlyName: "Data Type",
|
|
1873
|
+
shortName: "d",
|
|
1874
|
+
examples: ["User", "Product", "Task"],
|
|
1875
|
+
helpText: "The Wasp entity or model name this action will work with"
|
|
1876
|
+
});
|
|
1877
|
+
var schema2 = z3.object({
|
|
1878
|
+
feature: commonSchemas.feature,
|
|
1879
|
+
operation: actionSchema,
|
|
1880
|
+
dataType: dataTypeSchema,
|
|
1881
|
+
name: extend2(commonSchemas.name.optional(), commonSchemas.name._metadata),
|
|
1882
|
+
entities: commonSchemas.entities,
|
|
1883
|
+
force: commonSchemas.force,
|
|
1884
|
+
auth: commonSchemas.auth
|
|
1885
|
+
});
|
|
1886
|
+
|
|
1887
|
+
// src/generators/action/action-generator.ts
|
|
1888
|
+
var ActionGenerator = class extends OperationGeneratorBase {
|
|
1889
|
+
get entityType() {
|
|
1890
|
+
return CONFIG_TYPES.ACTION;
|
|
1891
|
+
}
|
|
1892
|
+
description = "Generate actions (mutations) for Wasp applications";
|
|
1893
|
+
schema = schema2;
|
|
1894
|
+
async generate(flags) {
|
|
1895
|
+
const { dataType, feature, name } = flags;
|
|
1896
|
+
const operation = flags.operation;
|
|
1897
|
+
const operationType = "action";
|
|
1898
|
+
const entities = flags.entities ? Array.isArray(flags.entities) ? flags.entities : flags.entities.split(",").map((e) => e.trim()).filter(Boolean) : [];
|
|
1899
|
+
if (dataType && !entities.includes(dataType)) {
|
|
1900
|
+
entities.unshift(dataType);
|
|
1901
|
+
}
|
|
1902
|
+
const { operationCode, operationName } = await this.generateOperationComponents(
|
|
1903
|
+
dataType,
|
|
1904
|
+
operation,
|
|
1905
|
+
flags.auth,
|
|
1906
|
+
entities,
|
|
1907
|
+
false,
|
|
1908
|
+
null,
|
|
1909
|
+
name
|
|
1910
|
+
);
|
|
1911
|
+
return this.handleGeneratorError(
|
|
1912
|
+
this.entityType,
|
|
1913
|
+
operationName,
|
|
1914
|
+
async () => {
|
|
1915
|
+
const configPath = this.validateFeatureConfig(feature);
|
|
1916
|
+
const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
|
|
1917
|
+
const importPath = `${importDirectory}/${operationName}`;
|
|
1918
|
+
this.generateOperationFile(
|
|
1919
|
+
operationsDir,
|
|
1920
|
+
operationName,
|
|
1921
|
+
operationCode,
|
|
1922
|
+
flags.force || false
|
|
1923
|
+
);
|
|
1924
|
+
const definition = this.getDefinition(
|
|
1925
|
+
operationName,
|
|
1926
|
+
feature,
|
|
1927
|
+
entities,
|
|
1928
|
+
"action",
|
|
1929
|
+
importPath,
|
|
1930
|
+
flags.auth
|
|
1931
|
+
);
|
|
1932
|
+
this.updateConfigWithCheck(
|
|
1933
|
+
configPath,
|
|
1934
|
+
"addAction",
|
|
1935
|
+
operationName,
|
|
1936
|
+
definition,
|
|
1937
|
+
feature,
|
|
1938
|
+
flags.force || false
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
// src/generators/api/api-generator.ts
|
|
1946
|
+
import { toCamelCase as toCamelCase2, toPascalCase as toPascalCase3 } from "@ingenyus/swarm";
|
|
1947
|
+
|
|
1948
|
+
// src/generators/api/schema.ts
|
|
1949
|
+
import { extend as extend3 } from "@ingenyus/swarm";
|
|
1950
|
+
import { z as z4 } from "zod";
|
|
1951
|
+
var validHttpMethods = Object.values(HTTP_METHODS);
|
|
1952
|
+
var schema3 = z4.object({
|
|
1953
|
+
method: extend3(
|
|
1954
|
+
z4.string().min(1, "HTTP method is required").refine(getTypedValueValidator(validHttpMethods), {
|
|
1955
|
+
message: `Invalid HTTP method. Must be one of: ${validHttpMethods.join(", ")}`
|
|
1956
|
+
}).transform(getTypedValueTransformer(validHttpMethods)),
|
|
1957
|
+
{
|
|
1958
|
+
description: "The HTTP method used for this API endpoint",
|
|
1959
|
+
friendlyName: "HTTP Method",
|
|
1960
|
+
shortName: "m",
|
|
1961
|
+
examples: validHttpMethods
|
|
1962
|
+
}
|
|
1963
|
+
),
|
|
1964
|
+
feature: commonSchemas.feature,
|
|
1965
|
+
name: commonSchemas.name,
|
|
1966
|
+
path: commonSchemas.path,
|
|
1967
|
+
entities: commonSchemas.entities,
|
|
1968
|
+
auth: commonSchemas.auth,
|
|
1969
|
+
force: commonSchemas.force,
|
|
1970
|
+
customMiddleware: z4.boolean().optional()
|
|
1971
|
+
});
|
|
1972
|
+
|
|
1973
|
+
// src/generators/api/api-generator.ts
|
|
1974
|
+
var ApiGenerator = class extends EntityGeneratorBase {
|
|
1975
|
+
get entityType() {
|
|
1976
|
+
return CONFIG_TYPES.API;
|
|
1977
|
+
}
|
|
1978
|
+
description = "Generate API endpoints for Wasp applications";
|
|
1979
|
+
schema = schema3;
|
|
1980
|
+
async generate(flags) {
|
|
1981
|
+
const apiName = toCamelCase2(flags?.name);
|
|
1982
|
+
return this.handleGeneratorError(this.entityType, apiName, async () => {
|
|
1983
|
+
const configPath = this.validateFeatureConfig(flags.feature);
|
|
1984
|
+
const {
|
|
1985
|
+
targetDirectory: apiTargetDirectory,
|
|
1986
|
+
importDirectory: apiImportDirectory
|
|
1987
|
+
} = this.ensureTargetDirectory(flags.feature, this.name);
|
|
1988
|
+
const fileName = `${apiName}.ts`;
|
|
1989
|
+
const targetFile = `${apiTargetDirectory}/${fileName}`;
|
|
1990
|
+
await this.generateApiFile(targetFile, apiName, flags);
|
|
1991
|
+
if (flags.customMiddleware) {
|
|
1992
|
+
const { targetDirectory: middlewareTargetDirectory } = this.ensureTargetDirectory(flags.feature, "middleware");
|
|
1993
|
+
const middlewareFile = `${middlewareTargetDirectory}/${apiName}.ts`;
|
|
1994
|
+
this.generateMiddlewareFile(
|
|
1995
|
+
middlewareFile,
|
|
1996
|
+
apiName,
|
|
1997
|
+
flags.force || false
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
await this.updateConfigFile(
|
|
2001
|
+
flags.feature,
|
|
2002
|
+
apiName,
|
|
2003
|
+
fileName,
|
|
2004
|
+
apiImportDirectory,
|
|
2005
|
+
flags,
|
|
2006
|
+
configPath
|
|
2007
|
+
);
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
async generateApiFile(targetFile, apiName, { method, auth = false, force = false }) {
|
|
2011
|
+
const replacements = this.buildTemplateData(apiName, method, auth);
|
|
2012
|
+
await this.renderTemplateToFile(
|
|
2013
|
+
"api.eta",
|
|
2014
|
+
replacements,
|
|
2015
|
+
targetFile,
|
|
2016
|
+
"API endpoint file",
|
|
2017
|
+
force
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
async updateConfigFile(featurePath, apiName, apiFile, importDirectory, flags, configFilePath) {
|
|
2021
|
+
const { force = false, entities, method, route, auth } = flags;
|
|
2022
|
+
const importPath = this.path.join(importDirectory, apiFile);
|
|
2023
|
+
const definition = await this.getConfigDefinition(
|
|
2024
|
+
apiName,
|
|
2025
|
+
featurePath,
|
|
2026
|
+
Array.isArray(entities) ? entities : entities ? [entities] : [],
|
|
2027
|
+
method,
|
|
2028
|
+
route,
|
|
2029
|
+
apiFile,
|
|
2030
|
+
auth,
|
|
2031
|
+
importPath,
|
|
2032
|
+
flags.customMiddleware || false
|
|
2033
|
+
);
|
|
2034
|
+
this.updateConfigWithCheck(
|
|
2035
|
+
configFilePath,
|
|
2036
|
+
"addApi",
|
|
2037
|
+
apiName,
|
|
2038
|
+
definition,
|
|
2039
|
+
featurePath,
|
|
2040
|
+
force
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
async getConfigDefinition(apiName, featurePath, entities, method, route, apiFile, auth = false, importPath, customMiddleware = false) {
|
|
2044
|
+
const featureDir = getFeatureImportPath(featurePath);
|
|
2045
|
+
const configTemplatePath = await this.getTemplatePath("config/api.eta");
|
|
2046
|
+
return this.templateUtility.processTemplate(configTemplatePath, {
|
|
2047
|
+
apiName,
|
|
2048
|
+
featureDir,
|
|
2049
|
+
entities: entities.map((e) => `"${e}"`).join(", "),
|
|
2050
|
+
method,
|
|
2051
|
+
route,
|
|
2052
|
+
apiFile,
|
|
2053
|
+
auth: String(auth),
|
|
2054
|
+
importPath,
|
|
2055
|
+
customMiddleware: String(customMiddleware)
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
buildTemplateData(apiName, method, auth) {
|
|
2059
|
+
const apiType = toPascalCase3(apiName);
|
|
2060
|
+
const authCheck = auth ? ` if (!context.user) {
|
|
2061
|
+
throw new HttpError(401);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
` : "";
|
|
2065
|
+
const methodCheck = method !== "ALL" ? ` if (req.method !== '${method}') {
|
|
2066
|
+
throw new HttpError(405);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
` : "";
|
|
2070
|
+
const errorImport = auth || method !== "ALL" ? 'import { HttpError } from "wasp/server";\n' : "";
|
|
2071
|
+
const imports = `${errorImport}import type { ${apiType} } from "wasp/server/api";`;
|
|
2072
|
+
return {
|
|
2073
|
+
imports,
|
|
2074
|
+
apiType,
|
|
2075
|
+
apiName,
|
|
2076
|
+
methodCheck,
|
|
2077
|
+
authCheck
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
// src/generators/api-namespace/api-namespace-generator.ts
|
|
2083
|
+
import { toCamelCase as toCamelCase3 } from "@ingenyus/swarm";
|
|
2084
|
+
import path9 from "path";
|
|
2085
|
+
|
|
2086
|
+
// src/generators/api-namespace/schema.ts
|
|
2087
|
+
import { z as z5 } from "zod";
|
|
2088
|
+
var schema4 = z5.object({
|
|
2089
|
+
feature: commonSchemas.feature,
|
|
2090
|
+
name: commonSchemas.name,
|
|
2091
|
+
path: commonSchemas.path,
|
|
2092
|
+
force: commonSchemas.force
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
// src/generators/api-namespace/api-namespace-generator.ts
|
|
2096
|
+
var ApiNamespaceGenerator = class extends EntityGeneratorBase {
|
|
2097
|
+
get entityType() {
|
|
2098
|
+
return CONFIG_TYPES.API_NAMESPACE;
|
|
2099
|
+
}
|
|
2100
|
+
description = "Generate API namespaces for Wasp applications";
|
|
2101
|
+
schema = schema4;
|
|
2102
|
+
async generate(flags) {
|
|
2103
|
+
const { name, path: apiPath, feature } = flags;
|
|
2104
|
+
const namespaceName = toCamelCase3(name);
|
|
2105
|
+
return this.handleGeneratorError(
|
|
2106
|
+
this.entityType,
|
|
2107
|
+
namespaceName,
|
|
2108
|
+
async () => {
|
|
2109
|
+
const configPath = this.validateFeatureConfig(feature);
|
|
2110
|
+
const { targetDirectory, importDirectory } = this.ensureTargetDirectory(
|
|
2111
|
+
feature,
|
|
2112
|
+
"middleware"
|
|
2113
|
+
);
|
|
2114
|
+
const targetFile = `${targetDirectory}/${namespaceName}.ts`;
|
|
2115
|
+
await this.generateMiddlewareFile(
|
|
2116
|
+
targetFile,
|
|
2117
|
+
namespaceName,
|
|
2118
|
+
flags.force || false
|
|
2119
|
+
);
|
|
2120
|
+
await this.updateConfigFile(
|
|
2121
|
+
feature,
|
|
2122
|
+
namespaceName,
|
|
2123
|
+
importDirectory,
|
|
2124
|
+
apiPath,
|
|
2125
|
+
flags,
|
|
2126
|
+
configPath
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
async updateConfigFile(feature, namespaceName, importDirectory, apiPath, flags, configFilePath) {
|
|
2132
|
+
const { force = false } = flags;
|
|
2133
|
+
const importPath = path9.join(importDirectory, namespaceName);
|
|
2134
|
+
const definition = await this.getDefinition(
|
|
2135
|
+
namespaceName,
|
|
2136
|
+
importPath,
|
|
2137
|
+
apiPath
|
|
2138
|
+
);
|
|
2139
|
+
this.updateConfigWithCheck(
|
|
2140
|
+
configFilePath,
|
|
2141
|
+
"addApiNamespace",
|
|
2142
|
+
namespaceName,
|
|
2143
|
+
definition,
|
|
2144
|
+
feature,
|
|
2145
|
+
force
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Generates an apiNamespace definition for the feature configuration.
|
|
2150
|
+
*/
|
|
2151
|
+
async getDefinition(namespaceName, middlewareImportPath, pathValue) {
|
|
2152
|
+
const templatePath = this.templateUtility.resolveTemplatePath(
|
|
2153
|
+
"config/api-namespace.eta",
|
|
2154
|
+
"api-namespace",
|
|
2155
|
+
import.meta.url
|
|
2156
|
+
);
|
|
2157
|
+
return this.templateUtility.processTemplate(templatePath, {
|
|
2158
|
+
namespaceName,
|
|
2159
|
+
middlewareImportPath,
|
|
2160
|
+
pathValue
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
|
|
2165
|
+
// src/generators/crud/crud-generator.ts
|
|
2166
|
+
import { getPlural as getPlural2, toCamelCase as toCamelCase4, toPascalCase as toPascalCase4 } from "@ingenyus/swarm";
|
|
2167
|
+
|
|
2168
|
+
// src/generators/crud/schema.ts
|
|
2169
|
+
import { extend as extend4 } from "@ingenyus/swarm";
|
|
2170
|
+
import { z as z6 } from "zod";
|
|
2171
|
+
var validCrudOperations = Object.values(CRUD_OPERATIONS);
|
|
2172
|
+
var publicOperations = getCrudOperationsArray();
|
|
2173
|
+
var overrideOperations = getCrudOperationsArray();
|
|
2174
|
+
var excludeOperations = getCrudOperationsArray();
|
|
2175
|
+
var schema5 = z6.object({
|
|
2176
|
+
feature: commonSchemas.feature,
|
|
2177
|
+
name: commonSchemas.name,
|
|
2178
|
+
public: extend4(publicOperations, {
|
|
2179
|
+
description: "Public CRUD operations (accessible without authentication)",
|
|
2180
|
+
friendlyName: "Public Operations",
|
|
2181
|
+
shortName: "b",
|
|
2182
|
+
examples: ["get,getAll", "create,update"],
|
|
2183
|
+
helpText: "Operations that can be accessed without authentication"
|
|
2184
|
+
}),
|
|
2185
|
+
override: extend4(overrideOperations, {
|
|
2186
|
+
description: "Override existing CRUD operations",
|
|
2187
|
+
friendlyName: "Override Operations",
|
|
2188
|
+
shortName: "o",
|
|
2189
|
+
examples: ["create,update"],
|
|
2190
|
+
helpText: "Operations to override if they already exist"
|
|
2191
|
+
}),
|
|
2192
|
+
exclude: extend4(excludeOperations, {
|
|
2193
|
+
description: "Exclude specific CRUD operations from generation",
|
|
2194
|
+
friendlyName: "Exclude Operations",
|
|
2195
|
+
shortName: "x",
|
|
2196
|
+
examples: ["delete", "update,delete"],
|
|
2197
|
+
helpText: "Operations to exclude from generation"
|
|
2198
|
+
}),
|
|
2199
|
+
force: commonSchemas.force
|
|
2200
|
+
});
|
|
2201
|
+
function getCrudOperationsArray() {
|
|
2202
|
+
return z6.string().optional().refine(getTypedArrayValidator(validCrudOperations), {
|
|
2203
|
+
message: `Must be one or more of: ${validCrudOperations.join(", ")}`
|
|
2204
|
+
}).transform(getTypedArrayTransformer(validCrudOperations));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
// src/generators/crud/crud-generator.ts
|
|
2208
|
+
var CRUD_OPERATIONS_LIST = [
|
|
2209
|
+
"get",
|
|
2210
|
+
"getAll",
|
|
2211
|
+
"create",
|
|
2212
|
+
"update",
|
|
2213
|
+
"delete"
|
|
2214
|
+
];
|
|
2215
|
+
var CrudGenerator = class extends OperationGeneratorBase {
|
|
2216
|
+
get entityType() {
|
|
2217
|
+
return CONFIG_TYPES.CRUD;
|
|
2218
|
+
}
|
|
2219
|
+
description = "Generate CRUD operations for Wasp applications";
|
|
2220
|
+
schema = schema5;
|
|
2221
|
+
async generate(flags) {
|
|
2222
|
+
const { dataType, feature } = flags;
|
|
2223
|
+
const crudName = toCamelCase4(getPlural2(dataType));
|
|
2224
|
+
return this.handleGeneratorError(this.entityType, crudName, async () => {
|
|
2225
|
+
const configPath = this.validateFeatureConfig(feature);
|
|
2226
|
+
const { targetDirectory } = this.ensureTargetDirectory(
|
|
2227
|
+
feature,
|
|
2228
|
+
this.entityType.toLowerCase()
|
|
2229
|
+
);
|
|
2230
|
+
const targetFile = `${targetDirectory}/${crudName}.ts`;
|
|
2231
|
+
const fileExists = this.fileSystem.existsSync(targetFile);
|
|
2232
|
+
if (!fileExists || flags.force) {
|
|
2233
|
+
const operations = await this.getOperationsCode(
|
|
2234
|
+
dataType,
|
|
2235
|
+
crudName,
|
|
2236
|
+
flags
|
|
2237
|
+
);
|
|
2238
|
+
await this.generateCrudFile(
|
|
2239
|
+
targetFile,
|
|
2240
|
+
crudName,
|
|
2241
|
+
dataType,
|
|
2242
|
+
operations,
|
|
2243
|
+
flags.force || false
|
|
2244
|
+
);
|
|
2245
|
+
}
|
|
2246
|
+
await this.updateConfigFile(
|
|
2247
|
+
feature,
|
|
2248
|
+
crudName,
|
|
2249
|
+
dataType,
|
|
2250
|
+
flags,
|
|
2251
|
+
configPath
|
|
2252
|
+
);
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
async generateCrudFile(targetFile, crudName, dataType, operations, force) {
|
|
2256
|
+
const imports = `import { type ${toPascalCase4(dataType)} } from "wasp/entities";
|
|
2257
|
+
import { HttpError } from "wasp/server";
|
|
2258
|
+
import { type ${toPascalCase4(crudName)} } from "wasp/server/crud";`;
|
|
2259
|
+
const replacements = {
|
|
2260
|
+
imports,
|
|
2261
|
+
operations
|
|
2262
|
+
};
|
|
2263
|
+
await this.renderTemplateToFile(
|
|
2264
|
+
"crud.eta",
|
|
2265
|
+
replacements,
|
|
2266
|
+
targetFile,
|
|
2267
|
+
"CRUD file",
|
|
2268
|
+
force
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
async updateConfigFile(feature, crudName, dataType, flags, configPath) {
|
|
2272
|
+
const operations = this.buildOperations(flags);
|
|
2273
|
+
const definition = await this.getDefinition(crudName, dataType, operations);
|
|
2274
|
+
this.updateConfigWithCheck(
|
|
2275
|
+
configPath,
|
|
2276
|
+
"addCrud",
|
|
2277
|
+
crudName,
|
|
2278
|
+
definition,
|
|
2279
|
+
feature,
|
|
2280
|
+
flags.force || false
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
buildOperations(flags) {
|
|
2284
|
+
const {
|
|
2285
|
+
public: publicOps = [],
|
|
2286
|
+
override: overrideOps = [],
|
|
2287
|
+
exclude: excludeOps = []
|
|
2288
|
+
} = flags;
|
|
2289
|
+
return CRUD_OPERATIONS_LIST.reduce(
|
|
2290
|
+
(acc, operation) => {
|
|
2291
|
+
if (excludeOps.includes(operation)) {
|
|
2292
|
+
return acc;
|
|
2293
|
+
}
|
|
2294
|
+
const operationConfig = {};
|
|
2295
|
+
if (publicOps.includes(operation)) {
|
|
2296
|
+
operationConfig.isPublic = true;
|
|
2297
|
+
}
|
|
2298
|
+
if (overrideOps.includes(operation)) {
|
|
2299
|
+
operationConfig.override = true;
|
|
2300
|
+
}
|
|
2301
|
+
acc[operation] = operationConfig;
|
|
2302
|
+
return acc;
|
|
2303
|
+
},
|
|
2304
|
+
{}
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Generates operation code for overridden CRUD operations and returns as a single string.
|
|
2309
|
+
*/
|
|
2310
|
+
async getOperationsCode(dataType, crudName, flags) {
|
|
2311
|
+
if (!flags.override || flags.override.length === 0) {
|
|
2312
|
+
return "";
|
|
2313
|
+
}
|
|
2314
|
+
const operationCodes = [];
|
|
2315
|
+
for (const operation of flags.override) {
|
|
2316
|
+
const { operationCode } = await this.generateOperationComponents(
|
|
2317
|
+
dataType,
|
|
2318
|
+
operation,
|
|
2319
|
+
flags.auth || false,
|
|
2320
|
+
[dataType],
|
|
2321
|
+
true,
|
|
2322
|
+
toPascalCase4(crudName)
|
|
2323
|
+
);
|
|
2324
|
+
operationCodes.push(operationCode.replace(/^[\r\n]/, ""));
|
|
2325
|
+
}
|
|
2326
|
+
return operationCodes.join("");
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Generates a CRUD definition for the feature configuration.
|
|
2330
|
+
*/
|
|
2331
|
+
getDefinition(crudName, dataType, operations) {
|
|
2332
|
+
const templatePath = this.templateUtility.resolveTemplatePath(
|
|
2333
|
+
"config/crud.eta",
|
|
2334
|
+
"crud",
|
|
2335
|
+
import.meta.url
|
|
2336
|
+
);
|
|
2337
|
+
const operationsStr = JSON.stringify(operations, null, 2).replace(/"([^"]+)":/g, "$1:").slice(1, -1).split("\n").filter((line) => line.trim() !== "").map((line, index) => index === 0 ? line.trimStart() : " " + line).join("\n");
|
|
2338
|
+
return this.templateUtility.processTemplate(templatePath, {
|
|
2339
|
+
crudName: toPascalCase4(crudName),
|
|
2340
|
+
dataType,
|
|
2341
|
+
operations: operationsStr
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
|
|
2346
|
+
// src/generators/job/job-generator.ts
|
|
2347
|
+
import { capitalise as capitalise2, toCamelCase as toCamelCase5 } from "@ingenyus/swarm";
|
|
2348
|
+
|
|
2349
|
+
// src/generators/job/schema.ts
|
|
2350
|
+
import { extend as extend5 } from "@ingenyus/swarm";
|
|
2351
|
+
import { z as z7 } from "zod";
|
|
2352
|
+
var cronSchema = extend5(
|
|
2353
|
+
z7.string().optional().refine(
|
|
2354
|
+
(val) => {
|
|
2355
|
+
if (!val) return true;
|
|
2356
|
+
const parts = val.trim().split(/\s+/);
|
|
2357
|
+
if (parts.length !== 5) return false;
|
|
2358
|
+
const [minute, hour, day, month, weekday] = parts;
|
|
2359
|
+
const validateCronField = (field, min, max) => {
|
|
2360
|
+
if (field === "*") return true;
|
|
2361
|
+
const rangeRegex = /^(\d+)(-(\d+))?(,(\d+)(-(\d+))?)*(\/(\d+))?$/;
|
|
2362
|
+
if (!rangeRegex.test(field)) return false;
|
|
2363
|
+
const items = field.split(",");
|
|
2364
|
+
for (const item of items) {
|
|
2365
|
+
if (item.includes("/")) {
|
|
2366
|
+
const [base, step] = item.split("/");
|
|
2367
|
+
const stepNum = parseInt(step, 10);
|
|
2368
|
+
if (isNaN(stepNum) || stepNum <= 0) return false;
|
|
2369
|
+
if (base === "*") continue;
|
|
2370
|
+
const baseNum = parseInt(base, 10);
|
|
2371
|
+
if (isNaN(baseNum) || baseNum < min || baseNum > max)
|
|
2372
|
+
return false;
|
|
2373
|
+
} else if (item.includes("-")) {
|
|
2374
|
+
const [start, end] = item.split("-");
|
|
2375
|
+
const startNum = parseInt(start, 10);
|
|
2376
|
+
const endNum = parseInt(end, 10);
|
|
2377
|
+
if (isNaN(startNum) || isNaN(endNum) || startNum < min || endNum > max || startNum > endNum)
|
|
2378
|
+
return false;
|
|
2379
|
+
} else {
|
|
2380
|
+
const num = parseInt(item, 10);
|
|
2381
|
+
if (isNaN(num) || num < min || num > max) return false;
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
return true;
|
|
2385
|
+
};
|
|
2386
|
+
return validateCronField(minute, 0, 59) && validateCronField(hour, 0, 23) && validateCronField(day, 1, 31) && validateCronField(month, 1, 12) && validateCronField(weekday, 0, 6);
|
|
2387
|
+
},
|
|
2388
|
+
{
|
|
2389
|
+
message: 'Cron expression must be a valid five-field format: (minute hour day month weekday), e.g. "0 9 * * *"'
|
|
2390
|
+
}
|
|
2391
|
+
),
|
|
2392
|
+
{
|
|
2393
|
+
description: "Cron schedule expression for the job",
|
|
2394
|
+
friendlyName: "Cron Schedule",
|
|
2395
|
+
shortName: "c",
|
|
2396
|
+
examples: ["0 9 * * *", "*/15 * * * *", "0 0 1 * *"],
|
|
2397
|
+
helpText: "Five-field cron expression: minute hour day month weekday"
|
|
2398
|
+
}
|
|
2399
|
+
);
|
|
2400
|
+
var argsSchema = extend5(
|
|
2401
|
+
z7.string().optional().refine(
|
|
2402
|
+
(val) => {
|
|
2403
|
+
if (!val) return true;
|
|
2404
|
+
try {
|
|
2405
|
+
const parsed = JSON.parse(val);
|
|
2406
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed);
|
|
2407
|
+
} catch {
|
|
2408
|
+
return false;
|
|
2409
|
+
}
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
message: "Args must be a valid JSON object string"
|
|
2413
|
+
}
|
|
2414
|
+
),
|
|
2415
|
+
{
|
|
2416
|
+
description: "Arguments to pass to the job function when executed",
|
|
2417
|
+
friendlyName: "Job Arguments",
|
|
2418
|
+
shortName: "a",
|
|
2419
|
+
examples: ['{"userId": 123}', '{"type": "cleanup", "batchSize": 100}'],
|
|
2420
|
+
helpText: "JSON object string that will be passed to the job function"
|
|
2421
|
+
}
|
|
2422
|
+
);
|
|
2423
|
+
var schema6 = z7.object({
|
|
2424
|
+
feature: commonSchemas.feature,
|
|
2425
|
+
name: commonSchemas.name,
|
|
2426
|
+
entities: commonSchemas.entities,
|
|
2427
|
+
cron: cronSchema,
|
|
2428
|
+
args: argsSchema,
|
|
2429
|
+
force: commonSchemas.force
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
// src/generators/job/job-generator.ts
|
|
2433
|
+
var JobGenerator = class extends EntityGeneratorBase {
|
|
2434
|
+
get entityType() {
|
|
2435
|
+
return CONFIG_TYPES.JOB;
|
|
2436
|
+
}
|
|
2437
|
+
description = "Generate job workers for Wasp applications";
|
|
2438
|
+
schema = schema6;
|
|
2439
|
+
async generate(flags) {
|
|
2440
|
+
const jobName = toCamelCase5(flags.name);
|
|
2441
|
+
return this.handleGeneratorError(this.entityType, jobName, async () => {
|
|
2442
|
+
const configPath = this.validateFeatureConfig(flags.feature);
|
|
2443
|
+
const { targetDirectory } = this.ensureTargetDirectory(
|
|
2444
|
+
flags.feature,
|
|
2445
|
+
this.entityType.toLowerCase()
|
|
2446
|
+
);
|
|
2447
|
+
const targetFile = `${targetDirectory}/${jobName}.ts`;
|
|
2448
|
+
await this.generateJobFile(targetFile, jobName, flags);
|
|
2449
|
+
this.updateConfigFile(flags.feature, jobName, flags, configPath);
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
async generateJobFile(targetFile, jobName, flags) {
|
|
2453
|
+
const jobType = capitalise2(jobName);
|
|
2454
|
+
const entities = Array.isArray(flags.entities) ? flags.entities : flags.entities ? [flags.entities] : [];
|
|
2455
|
+
let imports = `import type { ${jobType} } from 'wasp/server/jobs';
|
|
2456
|
+
`;
|
|
2457
|
+
if (entities.length > 0) {
|
|
2458
|
+
imports += `import { ${entities.join(", ")} } from 'wasp/entities';
|
|
2459
|
+
`;
|
|
2460
|
+
}
|
|
2461
|
+
const replacements = {
|
|
2462
|
+
imports,
|
|
2463
|
+
jobType,
|
|
2464
|
+
jobName
|
|
2465
|
+
};
|
|
2466
|
+
await this.renderTemplateToFile(
|
|
2467
|
+
"job.eta",
|
|
2468
|
+
replacements,
|
|
2469
|
+
targetFile,
|
|
2470
|
+
"job worker",
|
|
2471
|
+
flags.force || false
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2474
|
+
updateConfigFile(featurePath, jobName, flags, configPath) {
|
|
2475
|
+
const entities = Array.isArray(flags.entities) ? flags.entities : flags.entities ? [flags.entities] : [];
|
|
2476
|
+
const cron = flags.cron || "0 0 * * *";
|
|
2477
|
+
const args = flags.args || "{}";
|
|
2478
|
+
const definition = this.getDefinition(
|
|
2479
|
+
jobName,
|
|
2480
|
+
entities,
|
|
2481
|
+
cron,
|
|
2482
|
+
args || "{}"
|
|
2483
|
+
);
|
|
2484
|
+
this.updateConfigWithCheck(
|
|
2485
|
+
configPath,
|
|
2486
|
+
"job",
|
|
2487
|
+
jobName,
|
|
2488
|
+
definition,
|
|
2489
|
+
featurePath,
|
|
2490
|
+
flags.force || false
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Generates a job definition for the feature configuration.
|
|
2495
|
+
*/
|
|
2496
|
+
getDefinition(jobName, entities, cron, args) {
|
|
2497
|
+
const templatePath = this.getDefaultTemplatePath("config/job.eta");
|
|
2498
|
+
return this.templateUtility.processTemplate(templatePath, {
|
|
2499
|
+
jobName,
|
|
2500
|
+
entities: entities.map((e) => `"${e}"`).join(", "),
|
|
2501
|
+
cron,
|
|
2502
|
+
args
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
|
|
2507
|
+
// src/generators/query/schema.ts
|
|
2508
|
+
import { extend as extend6 } from "@ingenyus/swarm";
|
|
2509
|
+
import { z as z8 } from "zod";
|
|
2510
|
+
var validQueries = Object.values(QUERY_OPERATIONS);
|
|
2511
|
+
var querySchema = extend6(
|
|
2512
|
+
z8.string().min(1, "Query type is required").refine(getTypedValueValidator(validQueries), {
|
|
2513
|
+
message: `Invalid query. Must be one of: ${validQueries.join(", ")}`
|
|
2514
|
+
}).transform(getTypedValueTransformer(validQueries)),
|
|
2515
|
+
{
|
|
2516
|
+
description: "The query operation to generate",
|
|
2517
|
+
friendlyName: "Query Operation",
|
|
2518
|
+
shortName: "o",
|
|
2519
|
+
examples: validQueries,
|
|
2520
|
+
helpText: "Available queries: get, getAll, getFiltered"
|
|
2521
|
+
}
|
|
2522
|
+
);
|
|
2523
|
+
var dataTypeSchema2 = extend6(z8.string().min(1, "Data type is required"), {
|
|
2524
|
+
description: "The data type/model name for this query",
|
|
2525
|
+
friendlyName: "Data Type",
|
|
2526
|
+
shortName: "d",
|
|
2527
|
+
examples: ["User", "Product", "Task"],
|
|
2528
|
+
helpText: "The Wasp entity or model name this query will work with"
|
|
2529
|
+
});
|
|
2530
|
+
var schema7 = z8.object({
|
|
2531
|
+
feature: commonSchemas.feature,
|
|
2532
|
+
operation: querySchema,
|
|
2533
|
+
dataType: dataTypeSchema2,
|
|
2534
|
+
name: extend6(commonSchemas.name.optional(), commonSchemas.name._metadata),
|
|
2535
|
+
entities: commonSchemas.entities,
|
|
2536
|
+
force: commonSchemas.force,
|
|
2537
|
+
auth: commonSchemas.auth
|
|
2538
|
+
});
|
|
2539
|
+
|
|
2540
|
+
// src/generators/query/query-generator.ts
|
|
2541
|
+
var QueryGenerator = class extends OperationGeneratorBase {
|
|
2542
|
+
get entityType() {
|
|
2543
|
+
return CONFIG_TYPES.QUERY;
|
|
2544
|
+
}
|
|
2545
|
+
description = "Generate queries (data fetching) for Wasp applications";
|
|
2546
|
+
schema = schema7;
|
|
2547
|
+
async generate(flags) {
|
|
2548
|
+
const { dataType, feature, name } = flags;
|
|
2549
|
+
const operation = flags.operation;
|
|
2550
|
+
const operationType = "query";
|
|
2551
|
+
const entities = flags.entities ? Array.isArray(flags.entities) ? flags.entities : flags.entities.split(",").map((e) => e.trim()).filter(Boolean) : [];
|
|
2552
|
+
if (dataType && !entities.includes(dataType)) {
|
|
2553
|
+
entities.unshift(dataType);
|
|
2554
|
+
}
|
|
2555
|
+
const { operationCode, operationName } = await this.generateOperationComponents(
|
|
2556
|
+
dataType,
|
|
2557
|
+
operation,
|
|
2558
|
+
flags.auth,
|
|
2559
|
+
entities,
|
|
2560
|
+
false,
|
|
2561
|
+
null,
|
|
2562
|
+
name
|
|
2563
|
+
);
|
|
2564
|
+
return this.handleGeneratorError(
|
|
2565
|
+
this.entityType,
|
|
2566
|
+
operationName,
|
|
2567
|
+
async () => {
|
|
2568
|
+
const configPath = this.validateFeatureConfig(feature);
|
|
2569
|
+
const { targetDirectory: operationsDir, importDirectory } = this.ensureTargetDirectory(feature, operationType);
|
|
2570
|
+
const importPath = `${importDirectory}/${operationName}`;
|
|
2571
|
+
this.generateOperationFile(
|
|
2572
|
+
operationsDir,
|
|
2573
|
+
operationName,
|
|
2574
|
+
operationCode,
|
|
2575
|
+
flags.force || false
|
|
2576
|
+
);
|
|
2577
|
+
const definition = this.getDefinition(
|
|
2578
|
+
operationName,
|
|
2579
|
+
feature,
|
|
2580
|
+
entities,
|
|
2581
|
+
"query",
|
|
2582
|
+
importPath,
|
|
2583
|
+
flags.auth
|
|
2584
|
+
);
|
|
2585
|
+
this.updateConfigWithCheck(
|
|
2586
|
+
configPath,
|
|
2587
|
+
"addQuery",
|
|
2588
|
+
operationName,
|
|
2589
|
+
definition,
|
|
2590
|
+
feature,
|
|
2591
|
+
flags.force || false
|
|
2592
|
+
);
|
|
2593
|
+
}
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
};
|
|
2597
|
+
|
|
2598
|
+
// src/generators/route/route-generator.ts
|
|
2599
|
+
import { formatDisplayName, toCamelCase as toCamelCase6, toPascalCase as toPascalCase5 } from "@ingenyus/swarm";
|
|
2600
|
+
|
|
2601
|
+
// src/generators/route/schema.ts
|
|
2602
|
+
import { z as z9 } from "zod";
|
|
2603
|
+
var schema8 = z9.object({
|
|
2604
|
+
feature: commonSchemas.feature,
|
|
2605
|
+
name: commonSchemas.name,
|
|
2606
|
+
path: commonSchemas.path,
|
|
2607
|
+
auth: commonSchemas.auth,
|
|
2608
|
+
force: commonSchemas.force
|
|
2609
|
+
});
|
|
2610
|
+
|
|
2611
|
+
// src/generators/route/route-generator.ts
|
|
2612
|
+
var RouteGenerator = class extends EntityGeneratorBase {
|
|
2613
|
+
get entityType() {
|
|
2614
|
+
return CONFIG_TYPES.ROUTE;
|
|
2615
|
+
}
|
|
2616
|
+
description = "Generate route handlers for Wasp applications";
|
|
2617
|
+
schema = schema8;
|
|
2618
|
+
async generate(flags) {
|
|
2619
|
+
const { path: routePath, name, feature } = flags;
|
|
2620
|
+
const routeName = toCamelCase6(name || getRouteNameFromPath(routePath));
|
|
2621
|
+
const componentName = toPascalCase5(routeName);
|
|
2622
|
+
const fileName = `${componentName}.tsx`;
|
|
2623
|
+
return this.handleGeneratorError(this.entityType, routeName, async () => {
|
|
2624
|
+
const configPath = this.validateFeatureConfig(feature);
|
|
2625
|
+
const { targetDirectory } = this.ensureTargetDirectory(feature, "page");
|
|
2626
|
+
const targetFile = `${targetDirectory}/${fileName}`;
|
|
2627
|
+
await this.generatePageFile(targetFile, componentName, flags);
|
|
2628
|
+
this.updateConfigFile(feature, routeName, routePath, flags, configPath);
|
|
2629
|
+
});
|
|
2630
|
+
}
|
|
2631
|
+
async generatePageFile(targetFile, componentName, flags) {
|
|
2632
|
+
const templatePath = "files/client/page.eta";
|
|
2633
|
+
const replacements = {
|
|
2634
|
+
componentName,
|
|
2635
|
+
displayName: formatDisplayName(componentName)
|
|
2636
|
+
};
|
|
2637
|
+
await this.renderTemplateToFile(
|
|
2638
|
+
"page.eta",
|
|
2639
|
+
replacements,
|
|
2640
|
+
targetFile,
|
|
2641
|
+
"Page file",
|
|
2642
|
+
flags.force || false
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
updateConfigFile(featurePath, routeName, routePath, flags, configPath) {
|
|
2646
|
+
const definition = this.getDefinition(
|
|
2647
|
+
routeName,
|
|
2648
|
+
routePath,
|
|
2649
|
+
featurePath,
|
|
2650
|
+
flags.auth
|
|
2651
|
+
);
|
|
2652
|
+
this.updateConfigWithCheck(
|
|
2653
|
+
configPath,
|
|
2654
|
+
"addRoute",
|
|
2655
|
+
routeName,
|
|
2656
|
+
definition,
|
|
2657
|
+
featurePath,
|
|
2658
|
+
flags.force || false
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Generates a route definition for the feature configuration.
|
|
2663
|
+
*/
|
|
2664
|
+
getDefinition(routeName, routePath, featurePath, auth = false) {
|
|
2665
|
+
const templatePath = this.getDefaultTemplatePath("config/route.eta");
|
|
2666
|
+
return this.templateUtility.processTemplate(templatePath, {
|
|
2667
|
+
featureName: featurePath.split("/").pop() || featurePath,
|
|
2668
|
+
routeName,
|
|
2669
|
+
routePath,
|
|
2670
|
+
auth: String(auth)
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
};
|
|
2674
|
+
export {
|
|
2675
|
+
ActionGenerator,
|
|
2676
|
+
ApiGenerator,
|
|
2677
|
+
ApiNamespaceGenerator,
|
|
2678
|
+
App,
|
|
2679
|
+
CrudGenerator,
|
|
2680
|
+
FeatureDirectoryGenerator,
|
|
2681
|
+
JobGenerator,
|
|
2682
|
+
QueryGenerator,
|
|
2683
|
+
RouteGenerator,
|
|
2684
|
+
TemplateUtility,
|
|
2685
|
+
WaspConfigGenerator,
|
|
2686
|
+
commonSchemas,
|
|
2687
|
+
copyDirectory,
|
|
2688
|
+
createWaspPlugin,
|
|
2689
|
+
ensureDirectoryExists,
|
|
2690
|
+
featureExists,
|
|
2691
|
+
findWaspRoot,
|
|
2692
|
+
generateIntersectionType,
|
|
2693
|
+
generateJsonTypeHandling,
|
|
2694
|
+
generateOmitType,
|
|
2695
|
+
generatePartialType,
|
|
2696
|
+
generatePickType,
|
|
2697
|
+
getAppRootDir,
|
|
2698
|
+
getConfigDir,
|
|
2699
|
+
getEntityMetadata,
|
|
2700
|
+
getFeatureDir,
|
|
2701
|
+
getFeatureImportPath,
|
|
2702
|
+
getFeatureTargetDir,
|
|
2703
|
+
getIdFields,
|
|
2704
|
+
getJsonFields,
|
|
2705
|
+
getOptionalFields,
|
|
2706
|
+
getPluginVersion,
|
|
2707
|
+
getRequiredFields,
|
|
2708
|
+
getRouteNameFromPath,
|
|
2709
|
+
getTemplatesDir,
|
|
2710
|
+
getTypedArrayTransformer,
|
|
2711
|
+
getTypedArrayValidator,
|
|
2712
|
+
getTypedValueTransformer,
|
|
2713
|
+
getTypedValueValidator,
|
|
2714
|
+
needsPrismaImport,
|
|
2715
|
+
normaliseFeaturePath,
|
|
2716
|
+
realFileSystem,
|
|
2717
|
+
wasp
|
|
2718
|
+
};
|