@flink-app/flink 0.3.12 → 0.4.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/.vscode/notes.txt +11 -0
- package/cli/run.ts +23 -27
- package/dist/bin/flink.js +2 -2
- package/dist/cli/build.js +3 -3
- package/dist/cli/clean.js +2 -2
- package/dist/cli/cli-utils.js +1 -1
- package/dist/cli/generate-schemas.js +20 -16
- package/dist/cli/run.js +3 -7
- package/dist/src/FlinkApp.d.ts +21 -3
- package/dist/src/FlinkApp.js +163 -53
- package/dist/src/FlinkErrors.d.ts +1 -1
- package/dist/src/FlinkErrors.js +5 -5
- package/dist/src/FlinkHttpHandler.d.ts +7 -7
- package/dist/src/FlinkJob.d.ts +58 -0
- package/dist/src/FlinkJob.js +2 -0
- package/dist/src/FlinkLog.d.ts +16 -0
- package/dist/src/FlinkLog.js +11 -30
- package/dist/src/FlinkRepo.js +1 -1
- package/dist/src/FlinkResponse.d.ts +2 -2
- package/dist/src/FsUtils.js +4 -4
- package/dist/src/TypeScriptCompiler.d.ts +5 -1
- package/dist/src/TypeScriptCompiler.js +119 -91
- package/dist/src/TypeScriptUtils.js +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +7 -2
- package/dist/src/mock-data-generator.js +1 -1
- package/dist/src/utils.js +9 -9
- package/package.json +7 -3
- package/spec/mock-project/dist/src/handlers/GetCar.js +59 -0
- package/spec/mock-project/dist/src/handlers/GetCar2.js +61 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema.js +55 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema2.js +55 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithArraySchema3.js +55 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema.js +57 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithLiteralSchema2.js +57 -0
- package/{dist/cli/generate.js → spec/mock-project/dist/src/handlers/GetCarWithOmitSchema.js} +19 -14
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile.js +60 -0
- package/spec/mock-project/dist/src/handlers/GetCarWithSchemaInFile2.js +60 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler.js +56 -0
- package/spec/mock-project/dist/src/handlers/ManuallyAddedHandler2.js +58 -0
- package/spec/mock-project/dist/src/handlers/PostCar.js +57 -0
- package/{dist/cli/generate-schema.js → spec/mock-project/dist/src/handlers/PostLogin.js} +18 -14
- package/spec/mock-project/dist/src/handlers/PutCar.js +57 -0
- package/spec/mock-project/dist/src/index.js +79 -0
- package/spec/mock-project/dist/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/src/schemas/FileWithTwoSchemas.js +2 -0
- package/src/FlinkApp.ts +146 -11
- package/src/FlinkJob.ts +64 -0
- package/src/FlinkLog.ts +13 -11
- package/src/TypeScriptCompiler.ts +593 -663
- package/src/index.ts +2 -1
- package/dist/cli/generate-schema.d.ts +0 -2
- package/dist/cli/generate.d.ts +0 -2
- package/dist/src/FlinkTsParser.d.ts +0 -2
- package/dist/src/FlinkTsParser.js +0 -219
- package/dist/src/FlinkTsUtils.d.ts +0 -16
- package/dist/src/FlinkTsUtils.js +0 -118
|
@@ -2,749 +2,679 @@ import { promises as fsPromises } from "fs";
|
|
|
2
2
|
import { JSONSchema7 } from "json-schema";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import glob from "tiny-glob";
|
|
5
|
+
import { Config, createFormatter, createParser, Schema, SchemaGenerator } from "ts-json-schema-generator";
|
|
5
6
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
SourceFile,
|
|
19
|
-
Symbol,
|
|
20
|
-
SyntaxKind,
|
|
21
|
-
ts,
|
|
22
|
-
Type,
|
|
23
|
-
TypeReferenceNode,
|
|
24
|
-
VariableDeclarationKind,
|
|
7
|
+
ArrayLiteralExpression,
|
|
8
|
+
DiagnosticCategory,
|
|
9
|
+
ImportDeclarationStructure,
|
|
10
|
+
OptionalKind,
|
|
11
|
+
Project,
|
|
12
|
+
SourceFile,
|
|
13
|
+
Symbol,
|
|
14
|
+
SyntaxKind,
|
|
15
|
+
ts,
|
|
16
|
+
Type,
|
|
17
|
+
TypeReferenceNode,
|
|
18
|
+
VariableDeclarationKind,
|
|
25
19
|
} from "ts-morph";
|
|
26
20
|
import { writeJsonFile } from "./FsUtils";
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
getDefaultExport,
|
|
30
|
-
getInterfaceName,
|
|
31
|
-
getTypeMetadata,
|
|
32
|
-
getTypesToImport,
|
|
33
|
-
} from "./TypeScriptUtils";
|
|
34
|
-
import {
|
|
35
|
-
deRefSchema,
|
|
36
|
-
getCollectionNameForRepo,
|
|
37
|
-
getHttpMethodFromHandlerName,
|
|
38
|
-
getRepoInstanceName,
|
|
39
|
-
} from "./utils";
|
|
21
|
+
import { addImports, getDefaultExport, getInterfaceName, getTypeMetadata, getTypesToImport } from "./TypeScriptUtils";
|
|
22
|
+
import { deRefSchema, getCollectionNameForRepo, getHttpMethodFromHandlerName, getRepoInstanceName } from "./utils";
|
|
40
23
|
|
|
41
24
|
class TypeScriptCompiler {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"Loaded",
|
|
72
|
-
this.project.getSourceFiles().length,
|
|
73
|
-
"source file(s) from",
|
|
74
|
-
cwd
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Deletes all generated files.
|
|
80
|
-
* @param cwd
|
|
81
|
-
*/
|
|
82
|
-
static async clean(cwd: string) {
|
|
83
|
-
const flinkDir = join(cwd, ".flink");
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await fsPromises.access(flinkDir);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
await fsPromises.mkdir(flinkDir);
|
|
25
|
+
private project: Project;
|
|
26
|
+
private schemaGenerator?: SchemaGenerator;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parsed typescript schemas that will be added to intermediate
|
|
30
|
+
* schemas.ts file.
|
|
31
|
+
*
|
|
32
|
+
* This will be written to file in a batch for performance reasons.
|
|
33
|
+
*/
|
|
34
|
+
private parsedTsSchemas: string[] = [];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Imports needed for schemas.ts.
|
|
38
|
+
*
|
|
39
|
+
* This will be added to file in a batch for performance reasons.
|
|
40
|
+
*/
|
|
41
|
+
private tsSchemasSymbolsToImports: Symbol[] = [];
|
|
42
|
+
|
|
43
|
+
constructor(private cwd: string) {
|
|
44
|
+
this.project = new Project({
|
|
45
|
+
tsConfigFilePath: join(cwd, "tsconfig.json"),
|
|
46
|
+
compilerOptions: {
|
|
47
|
+
noEmit: false,
|
|
48
|
+
outDir: join(cwd, "dist"),
|
|
49
|
+
// incremental: true,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
|
|
89
54
|
}
|
|
90
55
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.project.emitSync();
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Catch any compilation errors. Will return false if any Errors
|
|
108
|
-
* exists. Warnings will be passed thru but logged.
|
|
109
|
-
*/
|
|
110
|
-
getPreEmitDiagnostics() {
|
|
111
|
-
const preEmitDiag = this.project.getPreEmitDiagnostics();
|
|
56
|
+
/**
|
|
57
|
+
* Deletes all generated files.
|
|
58
|
+
* @param cwd
|
|
59
|
+
*/
|
|
60
|
+
static async clean(cwd: string) {
|
|
61
|
+
const flinkDir = join(cwd, ".flink");
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await fsPromises.access(flinkDir);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
await fsPromises.mkdir(flinkDir);
|
|
67
|
+
}
|
|
112
68
|
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
const files = await glob(".flink/**/*.ts", { cwd });
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
await fsPromises.rm(join(cwd, file));
|
|
72
|
+
}
|
|
115
73
|
|
|
116
|
-
|
|
117
|
-
|
|
74
|
+
return flinkDir;
|
|
75
|
+
}
|
|
118
76
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
77
|
+
/**
|
|
78
|
+
* Emits compiled javascript source to dist folder
|
|
79
|
+
*/
|
|
80
|
+
emit() {
|
|
81
|
+
this.project.emitSync();
|
|
82
|
+
}
|
|
122
83
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Catch any compilation errors. Will return false if any Errors
|
|
86
|
+
* exists. Warnings will be passed thru but logged.
|
|
87
|
+
*/
|
|
88
|
+
getPreEmitDiagnostics() {
|
|
89
|
+
const preEmitDiag = this.project.getPreEmitDiagnostics();
|
|
90
|
+
|
|
91
|
+
if (preEmitDiag.length > 0) {
|
|
92
|
+
let hasError = false;
|
|
93
|
+
|
|
94
|
+
for (const diag of preEmitDiag) {
|
|
95
|
+
let message = diag.getMessageText();
|
|
96
|
+
|
|
97
|
+
while (typeof message !== "string") {
|
|
98
|
+
message = message.getMessageText();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (diag.getCategory() === DiagnosticCategory.Error) {
|
|
102
|
+
console.error(`[ERROR] ${diag.getSourceFile()?.getBaseName()} (line ${diag.getLineNumber()}):`, message);
|
|
103
|
+
hasError = true;
|
|
104
|
+
}
|
|
105
|
+
if (diag.getCategory() === DiagnosticCategory.Warning) {
|
|
106
|
+
console.warn(`[WARNING] ${diag.getSourceFile()?.getBaseName()} (line ${diag.getLineNumber()}):`, message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (hasError) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
139
113
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (hasError) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
114
|
+
return true;
|
|
145
115
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
["generatedHandlers.ts"],
|
|
160
|
-
`// Generated ${new Date()}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scans project for handlers and add those to Flink
|
|
119
|
+
* "singleton" property `autoRegisteredHandlers` so they can
|
|
120
|
+
* be registered during start.
|
|
121
|
+
*
|
|
122
|
+
* Also extract handlers request and response schemas from Handler
|
|
123
|
+
* type arguments.
|
|
124
|
+
*/
|
|
125
|
+
async parseHandlers(excludeDirs: string[] = []) {
|
|
126
|
+
const generatedFile = this.createSourceFile(
|
|
127
|
+
["generatedHandlers.ts"],
|
|
128
|
+
`// Generated ${new Date()}
|
|
161
129
|
import { autoRegisteredHandlers, HttpMethod } from "@flink-app/flink";
|
|
162
130
|
export const handlers = [];
|
|
163
131
|
autoRegisteredHandlers.push(...handlers);
|
|
164
132
|
`
|
|
165
|
-
);
|
|
166
|
-
const handlersArr = generatedFile
|
|
167
|
-
.getVariableDeclarationOrThrow("handlers")
|
|
168
|
-
.getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
169
|
-
|
|
170
|
-
const handlers = await this.parseHandlerDir(generatedFile, handlersArr);
|
|
171
|
-
|
|
172
|
-
generatedFile.addImportDeclarations(handlers.imports);
|
|
173
|
-
|
|
174
|
-
await generatedFile.save();
|
|
175
|
-
|
|
176
|
-
await this.createIntermediateSchemaFile();
|
|
177
|
-
|
|
178
|
-
const jsonSchemas = await this.generateAndSaveJsonSchemas(
|
|
179
|
-
handlers.schemasToGenerate
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
this.appendSchemasToHandlerSourceFiles(
|
|
183
|
-
handlers.schemasToGenerate,
|
|
184
|
-
jsonSchemas
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
return generatedFile;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Scan `/src/handlers/*.ts` for handler files and register those.
|
|
192
|
-
*/
|
|
193
|
-
private async parseHandlerDir(
|
|
194
|
-
generatedFile: SourceFile,
|
|
195
|
-
handlersArr: ArrayLiteralExpression
|
|
196
|
-
) {
|
|
197
|
-
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
198
|
-
let i = 0;
|
|
199
|
-
const schemasToGenerate: {
|
|
200
|
-
reqSchemaType?: string;
|
|
201
|
-
resSchemaType?: string;
|
|
202
|
-
sourceFile: SourceFile;
|
|
203
|
-
}[] = [];
|
|
204
|
-
|
|
205
|
-
for (const sf of this.project.getSourceFiles()) {
|
|
206
|
-
if (!sf.getFilePath().includes("src/handlers/")) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const isAutoRegister = this.isAutoRegisterableHandler(sf);
|
|
211
|
-
|
|
212
|
-
console.log(
|
|
213
|
-
`Detected handler ${sf.getBaseName()} ${
|
|
214
|
-
!isAutoRegister ? "(requires manual registration)" : ""
|
|
215
|
-
}`
|
|
216
|
-
);
|
|
217
|
-
|
|
218
|
-
const namespaceImport =
|
|
219
|
-
sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
220
|
-
|
|
221
|
-
imports.push({
|
|
222
|
-
namespaceImport,
|
|
223
|
-
moduleSpecifier: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const assumedHttpMethod = getHttpMethodFromHandlerName(sf.getBaseName());
|
|
227
|
-
|
|
228
|
-
const schemaTypes = await this.extractSchemasFromHandlerSourceFile(sf);
|
|
229
|
-
|
|
230
|
-
// Append schemas and metadata to source file that will be part of emitted dist bundle (javascript)
|
|
231
|
-
sf.addVariableStatement({
|
|
232
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
233
|
-
isExported: true,
|
|
234
|
-
declarations: [
|
|
235
|
-
{
|
|
236
|
-
name: "__assumedHttpMethod",
|
|
237
|
-
initializer: `"${assumedHttpMethod || ""}"`,
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: "__file",
|
|
241
|
-
initializer: `"${sf.getBaseName()}"`,
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
name: "__query",
|
|
245
|
-
initializer: `[${(schemaTypes?.queryMetadata || [])
|
|
246
|
-
.map(
|
|
247
|
-
({ description, name }) =>
|
|
248
|
-
`{description: "${description}", name: "${name}"}`
|
|
249
|
-
)
|
|
250
|
-
.join(",")}]`,
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: "__params",
|
|
254
|
-
initializer: `[${(schemaTypes?.paramsMetadata || [])
|
|
255
|
-
.map(
|
|
256
|
-
({ description, name }) =>
|
|
257
|
-
`{description: "${description}", name: "${name}"}`
|
|
258
|
-
)
|
|
259
|
-
.join(",")}]`,
|
|
260
|
-
},
|
|
261
|
-
],
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
if (isAutoRegister) {
|
|
265
|
-
handlersArr.insertElement(
|
|
266
|
-
i,
|
|
267
|
-
`{handler: ${namespaceImport}, assumedHttpMethod: ${
|
|
268
|
-
assumedHttpMethod ? "HttpMethod." + assumedHttpMethod : undefined
|
|
269
|
-
}}`
|
|
270
133
|
);
|
|
271
|
-
|
|
272
|
-
|
|
134
|
+
const handlersArr = generatedFile.getVariableDeclarationOrThrow("handlers").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
135
|
+
|
|
136
|
+
const handlers = await this.parseHandlerDir(generatedFile, handlersArr);
|
|
137
|
+
|
|
138
|
+
generatedFile.addImportDeclarations(handlers.imports);
|
|
139
|
+
|
|
140
|
+
await generatedFile.save();
|
|
141
|
+
|
|
142
|
+
await this.createIntermediateSchemaFile();
|
|
143
|
+
|
|
144
|
+
const jsonSchemas = await this.generateAndSaveJsonSchemas(handlers.schemasToGenerate);
|
|
145
|
+
|
|
146
|
+
this.appendSchemasToHandlerSourceFiles(handlers.schemasToGenerate, jsonSchemas);
|
|
273
147
|
|
|
274
|
-
|
|
275
|
-
schemasToGenerate.push({ ...schemaTypes, sourceFile: sf });
|
|
276
|
-
}
|
|
148
|
+
return generatedFile;
|
|
277
149
|
}
|
|
278
150
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Scan `/src/handlers/*.ts` for handler files and register those.
|
|
153
|
+
*/
|
|
154
|
+
private async parseHandlerDir(generatedFile: SourceFile, handlersArr: ArrayLiteralExpression) {
|
|
155
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
156
|
+
let i = 0;
|
|
157
|
+
const schemasToGenerate: {
|
|
158
|
+
reqSchemaType?: string;
|
|
159
|
+
resSchemaType?: string;
|
|
160
|
+
sourceFile: SourceFile;
|
|
161
|
+
}[] = [];
|
|
162
|
+
|
|
163
|
+
for (const sf of this.project.getSourceFiles()) {
|
|
164
|
+
if (!sf.getFilePath().includes("src/handlers/")) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const isAutoRegister = this.isAutoRegisterableHandler(sf);
|
|
169
|
+
|
|
170
|
+
console.log(`Detected handler ${sf.getBaseName()} ${!isAutoRegister ? "(requires manual registration)" : ""}`);
|
|
171
|
+
|
|
172
|
+
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
173
|
+
|
|
174
|
+
imports.push({
|
|
175
|
+
namespaceImport,
|
|
176
|
+
moduleSpecifier: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const assumedHttpMethod = getHttpMethodFromHandlerName(sf.getBaseName());
|
|
180
|
+
|
|
181
|
+
const schemaTypes = await this.extractSchemasFromHandlerSourceFile(sf);
|
|
182
|
+
|
|
183
|
+
// Append schemas and metadata to source file that will be part of emitted dist bundle (javascript)
|
|
184
|
+
sf.addVariableStatement({
|
|
185
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
186
|
+
isExported: true,
|
|
187
|
+
declarations: [
|
|
188
|
+
{
|
|
189
|
+
name: "__assumedHttpMethod",
|
|
190
|
+
initializer: `"${assumedHttpMethod || ""}"`,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: "__file",
|
|
194
|
+
initializer: `"${sf.getBaseName()}"`,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "__query",
|
|
198
|
+
initializer: `[${(schemaTypes?.queryMetadata || [])
|
|
199
|
+
.map(({ description, name }) => `{description: "${description}", name: "${name}"}`)
|
|
200
|
+
.join(",")}]`,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "__params",
|
|
204
|
+
initializer: `[${(schemaTypes?.paramsMetadata || [])
|
|
205
|
+
.map(({ description, name }) => `{description: "${description}", name: "${name}"}`)
|
|
206
|
+
.join(",")}]`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (isAutoRegister) {
|
|
212
|
+
handlersArr.insertElement(
|
|
213
|
+
i,
|
|
214
|
+
`{handler: ${namespaceImport}, assumedHttpMethod: ${assumedHttpMethod ? "HttpMethod." + assumedHttpMethod : undefined}}`
|
|
215
|
+
);
|
|
216
|
+
i++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (schemaTypes) {
|
|
220
|
+
schemasToGenerate.push({ ...schemaTypes, sourceFile: sf });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
imports,
|
|
226
|
+
schemasToGenerate,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
284
229
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
230
|
+
async parseRepos() {
|
|
231
|
+
const generatedFile = this.createSourceFile(
|
|
232
|
+
["generatedRepos.ts"],
|
|
233
|
+
`// Generated ${new Date()}
|
|
289
234
|
import { autoRegisteredRepos } from "@flink-app/flink";
|
|
290
235
|
export const repos = [];
|
|
291
236
|
autoRegisteredRepos.push(...repos);
|
|
292
237
|
`
|
|
293
|
-
|
|
238
|
+
);
|
|
294
239
|
|
|
295
|
-
|
|
296
|
-
.getVariableDeclarationOrThrow("repos")
|
|
297
|
-
.getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
240
|
+
const reposArr = generatedFile.getVariableDeclarationOrThrow("repos").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
298
241
|
|
|
299
|
-
|
|
242
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
300
243
|
|
|
301
|
-
|
|
244
|
+
let i = 0;
|
|
302
245
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
246
|
+
for (const sf of this.project.getSourceFiles()) {
|
|
247
|
+
if (!sf.getFilePath().includes("src/repos/")) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
307
250
|
|
|
308
|
-
|
|
251
|
+
console.log(`Detected repo ${sf.getBaseName()}`);
|
|
309
252
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
253
|
+
imports.push({
|
|
254
|
+
defaultImport: sf.getBaseNameWithoutExtension(),
|
|
255
|
+
moduleSpecifier: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
|
|
256
|
+
});
|
|
314
257
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)}", Repo: ${sf.getBaseNameWithoutExtension()}}`
|
|
322
|
-
);
|
|
258
|
+
reposArr.insertElement(
|
|
259
|
+
i,
|
|
260
|
+
`{collectionName: "${getCollectionNameForRepo(sf.getBaseName())}", repoInstanceName: "${getRepoInstanceName(
|
|
261
|
+
sf.getBaseName()
|
|
262
|
+
)}", Repo: ${sf.getBaseNameWithoutExtension()}}`
|
|
263
|
+
);
|
|
323
264
|
|
|
324
|
-
|
|
325
|
-
|
|
265
|
+
i++;
|
|
266
|
+
}
|
|
326
267
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Generates a start script that will import references to handlers, repos and the
|
|
336
|
-
* actual Flink app to start.
|
|
337
|
-
*
|
|
338
|
-
* Note that order is of importance so generated metadata are imported and initialized before start of flink app.
|
|
339
|
-
* Otherwise singletons `autoRegisteredRepos` and `autoRegisteredHandlers` will not have been set.
|
|
340
|
-
*/
|
|
341
|
-
async generateStartScript(appEntryScript = "/src/index.ts") {
|
|
342
|
-
if (
|
|
343
|
-
!this.project.getSourceFile((sf) =>
|
|
344
|
-
sf.getFilePath().endsWith(appEntryScript)
|
|
345
|
-
)
|
|
346
|
-
) {
|
|
347
|
-
console.error(`Cannot find entry script '${appEntryScript}'`);
|
|
348
|
-
return process.exit(1);
|
|
268
|
+
generatedFile.addImportDeclarations(imports);
|
|
269
|
+
|
|
270
|
+
await generatedFile.save();
|
|
271
|
+
|
|
272
|
+
return generatedFile;
|
|
349
273
|
}
|
|
350
274
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Generates a start script that will import references to handlers, repos and the
|
|
277
|
+
* actual Flink app to start.
|
|
278
|
+
*
|
|
279
|
+
* Note that order is of importance so generated metadata are imported and initialized before start of flink app.
|
|
280
|
+
* Otherwise singletons `autoRegisteredRepos` and `autoRegisteredHandlers` will not have been set.
|
|
281
|
+
*/
|
|
282
|
+
async generateStartScript(appEntryScript = "/src/index.ts") {
|
|
283
|
+
if (!this.project.getSourceFile((sf) => sf.getFilePath().endsWith(appEntryScript))) {
|
|
284
|
+
console.error(`Cannot find entry script '${appEntryScript}'`);
|
|
285
|
+
return process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sf = this.createSourceFile(
|
|
289
|
+
["start.ts"],
|
|
290
|
+
`// Generated ${new Date()}
|
|
354
291
|
import "./generatedHandlers";
|
|
355
292
|
import "./generatedRepos";
|
|
293
|
+
import "./generatedJobs";
|
|
356
294
|
import "..${appEntryScript.replace(/\.ts/g, "")}";
|
|
357
295
|
`
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
{
|
|
370
|
-
overwrite: true,
|
|
371
|
-
}
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Parses handlers `Handler<...>` function and its type arguments to extract
|
|
377
|
-
* which schemas to use.
|
|
378
|
-
*
|
|
379
|
-
* There are multiple ways of defining schema types as valid ts and this
|
|
380
|
-
* implementation aims to support all of these.
|
|
381
|
-
*
|
|
382
|
-
* Some examples of different cases (check spec/mock-project/src/handlers for more):
|
|
383
|
-
*
|
|
384
|
-
* ```
|
|
385
|
-
* // Interface reference
|
|
386
|
-
* Handler<Ctx, Car>
|
|
387
|
-
* // Inline type definition with reference to interface
|
|
388
|
-
* Handler<Ctx, {car: Car}>
|
|
389
|
-
* // Inline type definition with literal values
|
|
390
|
-
* Handler<Ctx, {car: {model: string}}>
|
|
391
|
-
* // Array
|
|
392
|
-
* Handler<Ctx, Car[]>
|
|
393
|
-
* // Array with inline type definition
|
|
394
|
-
* Handler<Ctx, {car: Car}[]>
|
|
395
|
-
* ```
|
|
396
|
-
*
|
|
397
|
-
* Return names of req and/or res schema types.
|
|
398
|
-
*/
|
|
399
|
-
private async extractSchemasFromHandlerSourceFile(
|
|
400
|
-
handlerSourceFile: SourceFile
|
|
401
|
-
) {
|
|
402
|
-
const defaultExport = getDefaultExport(handlerSourceFile);
|
|
403
|
-
|
|
404
|
-
if (defaultExport) {
|
|
405
|
-
const handlerTypeRef = defaultExport.getFirstDescendantByKindOrThrow(
|
|
406
|
-
SyntaxKind.TypeReference
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
return this.extractSchemaTypeFromHandler(handlerTypeRef);
|
|
410
|
-
} else {
|
|
411
|
-
console.warn(
|
|
412
|
-
`Handler ${handlerSourceFile.getBaseName()} is missing default exported handler function`
|
|
413
|
-
);
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await sf.save();
|
|
299
|
+
|
|
300
|
+
return sf;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private createSourceFile(filename: string[], contents: string) {
|
|
304
|
+
return this.project.createSourceFile(join(this.cwd, ".flink", ...filename), contents, {
|
|
305
|
+
overwrite: true,
|
|
306
|
+
});
|
|
414
307
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Parses handlers `Handler<...>` function and its type arguments to extract
|
|
311
|
+
* which schemas to use.
|
|
312
|
+
*
|
|
313
|
+
* There are multiple ways of defining schema types as valid ts and this
|
|
314
|
+
* implementation aims to support all of these.
|
|
315
|
+
*
|
|
316
|
+
* Some examples of different cases (check spec/mock-project/src/handlers for more):
|
|
317
|
+
*
|
|
318
|
+
* ```
|
|
319
|
+
* // Interface reference
|
|
320
|
+
* Handler<Ctx, Car>
|
|
321
|
+
* // Inline type definition with reference to interface
|
|
322
|
+
* Handler<Ctx, {car: Car}>
|
|
323
|
+
* // Inline type definition with literal values
|
|
324
|
+
* Handler<Ctx, {car: {model: string}}>
|
|
325
|
+
* // Array
|
|
326
|
+
* Handler<Ctx, Car[]>
|
|
327
|
+
* // Array with inline type definition
|
|
328
|
+
* Handler<Ctx, {car: Car}[]>
|
|
329
|
+
* ```
|
|
330
|
+
*
|
|
331
|
+
* Return names of req and/or res schema types.
|
|
332
|
+
*/
|
|
333
|
+
private async extractSchemasFromHandlerSourceFile(handlerSourceFile: SourceFile) {
|
|
334
|
+
const defaultExport = getDefaultExport(handlerSourceFile);
|
|
335
|
+
|
|
336
|
+
if (defaultExport) {
|
|
337
|
+
const handlerTypeRef = defaultExport.getFirstDescendantByKindOrThrow(SyntaxKind.TypeReference);
|
|
338
|
+
|
|
339
|
+
return this.extractSchemaTypeFromHandler(handlerTypeRef);
|
|
340
|
+
} else {
|
|
341
|
+
console.warn(`Handler ${handlerSourceFile.getBaseName()} is missing default exported handler function`);
|
|
342
|
+
}
|
|
424
343
|
}
|
|
425
344
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
345
|
+
private async saveIntermediateTsSchema(schema: Type<ts.Type>, handlerFile: SourceFile, suffix: string) {
|
|
346
|
+
if (schema.isAny()) {
|
|
347
|
+
return; // 'any' indicates that no schema is used
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const handlerFileName = handlerFile.getBaseNameWithoutExtension().replace(/\./g, "_");
|
|
429
351
|
|
|
430
|
-
|
|
352
|
+
let generatedSchemaInterfaceStr = "";
|
|
431
353
|
|
|
432
|
-
|
|
354
|
+
const schemaInterfaceName = `${handlerFileName}_${suffix}`;
|
|
433
355
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
356
|
+
if (schema.isInterface()) {
|
|
357
|
+
/*
|
|
358
|
+
* Type argument is an interface. This should be normal case when
|
|
359
|
+
* schema is defined directly for example `Handler<Ctx, Car>`
|
|
360
|
+
*/
|
|
361
|
+
const schemaSymbol = schema.getSymbolOrThrow();
|
|
362
|
+
const interfaceName = getInterfaceName(schemaSymbol);
|
|
363
|
+
const declaration = schemaSymbol.getDeclarations()[0];
|
|
442
364
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
365
|
+
if (declaration.getSourceFile() === handlerFile) {
|
|
366
|
+
// Interface is declared within handler file
|
|
367
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} {
|
|
446
368
|
${schema
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
369
|
+
.getProperties()
|
|
370
|
+
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
371
|
+
.join("\n")}
|
|
450
372
|
}`;
|
|
451
373
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
374
|
+
for (const typeToImport of getTypesToImport(declaration)) {
|
|
375
|
+
this.tsSchemasSymbolsToImports.push(typeToImport.getSymbolOrThrow().getDeclaredType().getSymbolOrThrow());
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
// Interface is imported from other file
|
|
379
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends ${interfaceName} {}`;
|
|
380
|
+
this.tsSchemasSymbolsToImports.push(schemaSymbol);
|
|
381
|
+
}
|
|
382
|
+
} else if (schema.isArray()) {
|
|
383
|
+
const arrayTypeArg = schema.getTypeArguments()[0];
|
|
384
|
+
const schemaSymbol = arrayTypeArg.getSymbolOrThrow();
|
|
385
|
+
const interfaceName = schemaSymbol.getEscapedName();
|
|
386
|
+
const declaration = schemaSymbol.getDeclarations()[0];
|
|
387
|
+
|
|
388
|
+
if (declaration.getSourceFile() !== handlerFile) {
|
|
389
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<${interfaceName}> {}`;
|
|
390
|
+
this.tsSchemasSymbolsToImports.push(schemaSymbol);
|
|
391
|
+
} else {
|
|
392
|
+
if (arrayTypeArg.isInterface()) {
|
|
393
|
+
const props = arrayTypeArg
|
|
394
|
+
.getProperties()
|
|
395
|
+
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
396
|
+
.join(" ");
|
|
397
|
+
|
|
398
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<{${props}}> {}`;
|
|
399
|
+
} else {
|
|
400
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} extends Array<${declaration.getText()}> {}`;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const typeToImport of getTypesToImport(declaration)) {
|
|
404
|
+
this.tsSchemasSymbolsToImports.push(typeToImport.getSymbolOrThrow().getDeclaredType().getSymbolOrThrow());
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} else if (schema.isObject()) {
|
|
408
|
+
/*
|
|
409
|
+
* Schema is defined inline, for example `Handler<Ctx, {car: Car}>`
|
|
410
|
+
* We need extract `{car: Car}` into its own interface and make sure
|
|
411
|
+
* to import types if needed to
|
|
412
|
+
*/
|
|
413
|
+
const declaration = schema.getSymbolOrThrow().getDeclarations()[0];
|
|
414
|
+
|
|
415
|
+
const typeRefIdentifiers = declaration
|
|
416
|
+
.getDescendantsOfKind(SyntaxKind.TypeReference)
|
|
417
|
+
.map((typeRef) => typeRef.getFirstChildByKindOrThrow(SyntaxKind.Identifier));
|
|
418
|
+
|
|
419
|
+
typeRefIdentifiers.forEach((tr) => {
|
|
420
|
+
this.tsSchemasSymbolsToImports.push(tr.getSymbolOrThrow().getDeclaredType().getSymbolOrThrow());
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
generatedSchemaInterfaceStr = `export interface ${schemaInterfaceName} { ${schema
|
|
424
|
+
.getProperties()
|
|
425
|
+
.map((p) => p.getValueDeclarationOrThrow().getText())
|
|
426
|
+
.join("\n")} }`;
|
|
479
427
|
} else {
|
|
480
|
-
|
|
428
|
+
console.log("[WARN] Unknown schema type", schema.getText());
|
|
481
429
|
}
|
|
482
430
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
431
|
+
if (generatedSchemaInterfaceStr) {
|
|
432
|
+
this.parsedTsSchemas.push(generatedSchemaInterfaceStr);
|
|
433
|
+
|
|
434
|
+
return schemaInterfaceName;
|
|
487
435
|
}
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
/*
|
|
491
|
-
* Schema is defined inline, for example `Handler<Ctx, {car: Car}>`
|
|
492
|
-
* We need extract `{car: Car}` into its own interface and make sure
|
|
493
|
-
* to import types if needed to
|
|
494
|
-
*/
|
|
495
|
-
const declaration = schema.getSymbolOrThrow().getDeclarations()[0];
|
|
496
|
-
|
|
497
|
-
const typeRefIdentifiers = declaration
|
|
498
|
-
.getDescendantsOfKind(SyntaxKind.TypeReference)
|
|
499
|
-
.map((typeRef) =>
|
|
500
|
-
typeRef.getFirstChildByKindOrThrow(SyntaxKind.Identifier)
|
|
501
|
-
);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
502
438
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
} else {
|
|
514
|
-
console.log("[WARN] Unknown schema type", schema.getText());
|
|
439
|
+
private initJsonSchemaGenerator() {
|
|
440
|
+
const conf: Config = {
|
|
441
|
+
// expose: "",
|
|
442
|
+
// topRef: false,
|
|
443
|
+
};
|
|
444
|
+
const formatter = createFormatter(conf);
|
|
445
|
+
const parser = createParser(this.project.getProgram().compilerObject, conf);
|
|
446
|
+
const generator = new SchemaGenerator(this.project.getProgram().compilerObject, parser, formatter, conf);
|
|
447
|
+
|
|
448
|
+
return generator;
|
|
515
449
|
}
|
|
516
450
|
|
|
517
|
-
|
|
518
|
-
|
|
451
|
+
private async generateAndSaveJsonSchemas(schemas: { reqSchemaType?: string; resSchemaType?: string }[]) {
|
|
452
|
+
const jsonSchemas: Schema[] = [];
|
|
453
|
+
|
|
454
|
+
for (const { reqSchemaType, resSchemaType } of schemas) {
|
|
455
|
+
if (reqSchemaType) {
|
|
456
|
+
jsonSchemas.push(this.generateJsonSchema(reqSchemaType));
|
|
457
|
+
}
|
|
458
|
+
if (resSchemaType) {
|
|
459
|
+
jsonSchemas.push(this.generateJsonSchema(resSchemaType));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const mergedSchemas = jsonSchemas.reduce(
|
|
464
|
+
(out, schema) => {
|
|
465
|
+
if (schema.definitions) {
|
|
466
|
+
out.definitions = { ...out.definitions, ...schema.definitions };
|
|
467
|
+
}
|
|
468
|
+
return out;
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
472
|
+
$ref: "#/definitions/Schemas",
|
|
473
|
+
definitions: {},
|
|
474
|
+
}
|
|
475
|
+
);
|
|
519
476
|
|
|
520
|
-
|
|
477
|
+
const filePath = join(this.cwd, ".flink", "schemas", "schemas.json");
|
|
478
|
+
|
|
479
|
+
await writeJsonFile(filePath, mergedSchemas);
|
|
480
|
+
|
|
481
|
+
this.project.addSourceFileAtPath(filePath);
|
|
482
|
+
|
|
483
|
+
return mergedSchemas;
|
|
521
484
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
// topRef: false,
|
|
529
|
-
};
|
|
530
|
-
const formatter = createFormatter(conf);
|
|
531
|
-
const parser = createParser(this.project.getProgram().compilerObject, conf);
|
|
532
|
-
const generator = new SchemaGenerator(
|
|
533
|
-
this.project.getProgram().compilerObject,
|
|
534
|
-
parser,
|
|
535
|
-
formatter,
|
|
536
|
-
conf
|
|
537
|
-
);
|
|
538
|
-
|
|
539
|
-
return generator;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
private async generateAndSaveJsonSchemas(
|
|
543
|
-
schemas: { reqSchemaType?: string; resSchemaType?: string }[]
|
|
544
|
-
) {
|
|
545
|
-
const jsonSchemas: Schema[] = [];
|
|
546
|
-
|
|
547
|
-
for (const { reqSchemaType, resSchemaType } of schemas) {
|
|
548
|
-
if (reqSchemaType) {
|
|
549
|
-
jsonSchemas.push(this.generateJsonSchema(reqSchemaType));
|
|
550
|
-
}
|
|
551
|
-
if (resSchemaType) {
|
|
552
|
-
jsonSchemas.push(this.generateJsonSchema(resSchemaType));
|
|
553
|
-
}
|
|
485
|
+
|
|
486
|
+
private generateJsonSchema(typeName: string) {
|
|
487
|
+
if (!this.schemaGenerator) {
|
|
488
|
+
this.schemaGenerator = this.initJsonSchemaGenerator();
|
|
489
|
+
}
|
|
490
|
+
return this.schemaGenerator.createSchema(typeName);
|
|
554
491
|
}
|
|
555
492
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
493
|
+
private async extractSchemaTypeFromHandler(handlerTypeReference: TypeReferenceNode) {
|
|
494
|
+
// Name of Handler function - should be either `Handler` or `GetHandler`
|
|
495
|
+
const handlerType = handlerTypeReference.getTypeName().getText();
|
|
496
|
+
|
|
497
|
+
// Get type arguments a.k.a. generics which holds schemas such as this `Handler<Ctx, ReqSchema, ResSchema>`
|
|
498
|
+
const handlerTypeArgs = handlerTypeReference.getType().getAliasTypeArguments();
|
|
499
|
+
|
|
500
|
+
let reqSchema: Type<ts.Type> | undefined;
|
|
501
|
+
let resSchema: Type<ts.Type> | undefined;
|
|
502
|
+
let params: Type<ts.Type> | undefined;
|
|
503
|
+
let query: Type<ts.Type> | undefined;
|
|
504
|
+
|
|
505
|
+
if (handlerType === "Handler") {
|
|
506
|
+
// `Handler<Ctx, ReqSchema, ResSchema, Params, Query>`
|
|
507
|
+
// 0 = Ctx, 1 = Req schema, 2 = Res schema, 3 = Params, 4 = Query
|
|
508
|
+
reqSchema = handlerTypeArgs[1];
|
|
509
|
+
resSchema = handlerTypeArgs[2];
|
|
510
|
+
params = handlerTypeArgs[3];
|
|
511
|
+
query = handlerTypeArgs[4];
|
|
512
|
+
} else if (handlerType === "GetHandler") {
|
|
513
|
+
// `GetHandler<Ctx, ResSchema, Params, Query>`
|
|
514
|
+
// 0 = Ctx, 1 = Res schema, 2 = Params, 3 = Query
|
|
515
|
+
resSchema = handlerTypeArgs[1];
|
|
516
|
+
params = handlerTypeArgs[2];
|
|
517
|
+
query = handlerTypeArgs[3];
|
|
518
|
+
} else {
|
|
519
|
+
throw new Error(`Unknown handler type ${handlerType} in ${handlerTypeReference.getSourceFile().getBaseName()} - should be Handler or GetHandler`);
|
|
560
520
|
}
|
|
561
|
-
return out;
|
|
562
|
-
},
|
|
563
|
-
{
|
|
564
|
-
$schema: "http://json-schema.org/draft-07/schema#",
|
|
565
|
-
$ref: "#/definitions/Schemas",
|
|
566
|
-
definitions: {},
|
|
567
|
-
}
|
|
568
|
-
);
|
|
569
521
|
|
|
570
|
-
|
|
522
|
+
const sf = handlerTypeReference.getSourceFile();
|
|
571
523
|
|
|
572
|
-
|
|
524
|
+
const createReqSchemaPromise = reqSchema
|
|
525
|
+
? this.saveIntermediateTsSchema(reqSchema, sf, `${handlerTypeReference.getStartLineNumber()}_ReqSchema`)
|
|
526
|
+
: Promise.resolve("");
|
|
573
527
|
|
|
574
|
-
|
|
528
|
+
const createResSchemaPromise = resSchema
|
|
529
|
+
? this.saveIntermediateTsSchema(resSchema, sf, `${handlerTypeReference.getStartLineNumber()}_ResSchema`)
|
|
530
|
+
: Promise.resolve("");
|
|
575
531
|
|
|
576
|
-
|
|
577
|
-
}
|
|
532
|
+
const [reqSchemaType, resSchemaType] = await Promise.all([createReqSchemaPromise, createResSchemaPromise]);
|
|
578
533
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
private async extractSchemaTypeFromHandler(
|
|
587
|
-
handlerTypeReference: TypeReferenceNode
|
|
588
|
-
) {
|
|
589
|
-
// Name of Handler function - should be either `Handler` or `GetHandler`
|
|
590
|
-
const handlerType = handlerTypeReference.getTypeName().getText();
|
|
591
|
-
|
|
592
|
-
// Get type arguments a.k.a. generics which holds schemas such as this `Handler<Ctx, ReqSchema, ResSchema>`
|
|
593
|
-
const handlerTypeArgs = handlerTypeReference
|
|
594
|
-
.getType()
|
|
595
|
-
.getAliasTypeArguments();
|
|
596
|
-
|
|
597
|
-
let reqSchema: Type<ts.Type> | undefined;
|
|
598
|
-
let resSchema: Type<ts.Type> | undefined;
|
|
599
|
-
let params: Type<ts.Type> | undefined;
|
|
600
|
-
let query: Type<ts.Type> | undefined;
|
|
601
|
-
|
|
602
|
-
if (handlerType === "Handler") {
|
|
603
|
-
// `Handler<Ctx, ReqSchema, ResSchema, Params, Query>`
|
|
604
|
-
// 0 = Ctx, 1 = Req schema, 2 = Res schema, 3 = Params, 4 = Query
|
|
605
|
-
reqSchema = handlerTypeArgs[1];
|
|
606
|
-
resSchema = handlerTypeArgs[2];
|
|
607
|
-
params = handlerTypeArgs[3];
|
|
608
|
-
query = handlerTypeArgs[4];
|
|
609
|
-
} else if (handlerType === "GetHandler") {
|
|
610
|
-
// `GetHandler<Ctx, ResSchema, Params, Query>`
|
|
611
|
-
// 0 = Ctx, 1 = Res schema, 2 = Params, 3 = Query
|
|
612
|
-
resSchema = handlerTypeArgs[1];
|
|
613
|
-
params = handlerTypeArgs[2];
|
|
614
|
-
query = handlerTypeArgs[3];
|
|
615
|
-
} else {
|
|
616
|
-
throw new Error(
|
|
617
|
-
`Unknown handler type ${handlerType} in ${handlerTypeReference
|
|
618
|
-
.getSourceFile()
|
|
619
|
-
.getBaseName()} - should be Handler or GetHandler`
|
|
620
|
-
);
|
|
534
|
+
return {
|
|
535
|
+
reqSchemaType,
|
|
536
|
+
resSchemaType,
|
|
537
|
+
queryMetadata: getTypeMetadata(query),
|
|
538
|
+
paramsMetadata: getTypeMetadata(params),
|
|
539
|
+
};
|
|
621
540
|
}
|
|
622
541
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
: Promise.resolve("");
|
|
632
|
-
|
|
633
|
-
const createResSchemaPromise = resSchema
|
|
634
|
-
? this.saveIntermediateTsSchema(
|
|
635
|
-
resSchema,
|
|
636
|
-
sf,
|
|
637
|
-
`${handlerTypeReference.getStartLineNumber()}_ResSchema`
|
|
638
|
-
)
|
|
639
|
-
: Promise.resolve("");
|
|
640
|
-
|
|
641
|
-
const [reqSchemaType, resSchemaType] = await Promise.all([
|
|
642
|
-
createReqSchemaPromise,
|
|
643
|
-
createResSchemaPromise,
|
|
644
|
-
]);
|
|
645
|
-
|
|
646
|
-
return {
|
|
647
|
-
reqSchemaType,
|
|
648
|
-
resSchemaType,
|
|
649
|
-
queryMetadata: getTypeMetadata(query),
|
|
650
|
-
paramsMetadata: getTypeMetadata(params),
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
/**
|
|
655
|
-
* Creates generated source file that contains all
|
|
656
|
-
* TypeScript schemas that has been derived from handlers.
|
|
657
|
-
*/
|
|
658
|
-
private async createIntermediateSchemaFile() {
|
|
659
|
-
const schemaSourceFile = this.createSourceFile(
|
|
660
|
-
["schemas", `schemas.ts`],
|
|
661
|
-
`// Generated ${new Date()}
|
|
542
|
+
/**
|
|
543
|
+
* Creates generated source file that contains all
|
|
544
|
+
* TypeScript schemas that has been derived from handlers.
|
|
545
|
+
*/
|
|
546
|
+
private async createIntermediateSchemaFile() {
|
|
547
|
+
const schemaSourceFile = this.createSourceFile(
|
|
548
|
+
["schemas", `schemas.ts`],
|
|
549
|
+
`// Generated ${new Date()}
|
|
662
550
|
${this.parsedTsSchemas.join("\n\n")}`
|
|
663
|
-
|
|
551
|
+
);
|
|
664
552
|
|
|
665
|
-
|
|
553
|
+
addImports(schemaSourceFile, this.tsSchemasSymbolsToImports);
|
|
666
554
|
|
|
667
|
-
|
|
668
|
-
|
|
555
|
+
return schemaSourceFile.save();
|
|
556
|
+
}
|
|
669
557
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
558
|
+
/**
|
|
559
|
+
* Check if handler source file is up for auto registration by inspecting
|
|
560
|
+
* the Route.
|
|
561
|
+
*
|
|
562
|
+
* @param sf handler file
|
|
563
|
+
* @returns
|
|
564
|
+
*/
|
|
565
|
+
private isAutoRegisterableHandler(sf: SourceFile) {
|
|
566
|
+
const route = sf.getVariableDeclaration("Route");
|
|
567
|
+
|
|
568
|
+
if (!route) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
679
571
|
|
|
680
|
-
|
|
681
|
-
|
|
572
|
+
const routeProps = route.getFirstDescendantByKindOrThrow(SyntaxKind.ObjectLiteralExpression);
|
|
573
|
+
|
|
574
|
+
const skipAutoRegProp = routeProps.getProperty("skipAutoRegister");
|
|
575
|
+
|
|
576
|
+
return !skipAutoRegProp || skipAutoRegProp.getText().endsWith("false");
|
|
682
577
|
}
|
|
683
578
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
579
|
+
/**
|
|
580
|
+
* Appends generated json schemas to handler source files.
|
|
581
|
+
*
|
|
582
|
+
* @param handlers
|
|
583
|
+
* @param jsonSchemas
|
|
584
|
+
*/
|
|
585
|
+
private appendSchemasToHandlerSourceFiles(
|
|
586
|
+
handlers: {
|
|
587
|
+
sourceFile: SourceFile;
|
|
588
|
+
reqSchemaType?: string;
|
|
589
|
+
resSchemaType?: string;
|
|
590
|
+
}[],
|
|
591
|
+
jsonSchemas: JSONSchema7
|
|
592
|
+
) {
|
|
593
|
+
const jsonSchemaDefs = jsonSchemas.definitions || {};
|
|
594
|
+
|
|
595
|
+
for (const { sourceFile, reqSchemaType, resSchemaType } of handlers) {
|
|
596
|
+
if (reqSchemaType && !jsonSchemaDefs[reqSchemaType]) {
|
|
597
|
+
console.error(`Handler ${sourceFile.getBaseName} has request schema (${reqSchemaType}) defined, but JSON schema has been generated`);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (resSchemaType && !jsonSchemaDefs[resSchemaType]) {
|
|
602
|
+
console.error(`Handler ${sourceFile.getBaseName} has response schema (${resSchemaType}) defined, but JSON schema has been generated`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const reqJsonSchema = JSON.stringify(reqSchemaType ? deRefSchema(jsonSchemaDefs[reqSchemaType], jsonSchemas) : undefined);
|
|
607
|
+
const resJsonSchema = JSON.stringify(resSchemaType ? deRefSchema(jsonSchemaDefs[resSchemaType], jsonSchemas) : undefined);
|
|
608
|
+
|
|
609
|
+
sourceFile.addVariableStatement({
|
|
610
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
611
|
+
isExported: true,
|
|
612
|
+
declarations: [
|
|
613
|
+
{
|
|
614
|
+
name: "__schemas",
|
|
615
|
+
type: "any",
|
|
616
|
+
initializer: `{ reqSchema: ${reqJsonSchema}, resSchema: ${resJsonSchema} }`,
|
|
617
|
+
},
|
|
618
|
+
],
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
716
622
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
623
|
+
/**
|
|
624
|
+
* Scans project for jobs so they can be registered during start.
|
|
625
|
+
*/
|
|
626
|
+
async parseJobs() {
|
|
627
|
+
const generatedFile = this.createSourceFile(
|
|
628
|
+
["generatedJobs.ts"],
|
|
629
|
+
`// Generated ${new Date()}
|
|
630
|
+
import { autoRegisteredJobs } from "@flink-app/flink";
|
|
631
|
+
export const jobs = [];
|
|
632
|
+
autoRegisteredJobs.push(...jobs);
|
|
633
|
+
`
|
|
720
634
|
);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
635
|
+
|
|
636
|
+
const jobsArr = generatedFile.getVariableDeclarationOrThrow("jobs").getFirstDescendantByKindOrThrow(SyntaxKind.ArrayLiteralExpression);
|
|
637
|
+
|
|
638
|
+
const imports: OptionalKind<ImportDeclarationStructure>[] = [];
|
|
639
|
+
let i = 0;
|
|
640
|
+
|
|
641
|
+
for (const sf of this.project.getSourceFiles()) {
|
|
642
|
+
if (!sf.getFilePath().includes("src/jobs/")) {
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
console.log(`Detected job ${sf.getBaseName()}`);
|
|
647
|
+
|
|
648
|
+
const namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
649
|
+
|
|
650
|
+
imports.push({
|
|
651
|
+
namespaceImport,
|
|
652
|
+
moduleSpecifier: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Append metadata to source file that will be part of emitted dist bundle (javascript)
|
|
656
|
+
sf.addVariableStatement({
|
|
657
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
658
|
+
isExported: true,
|
|
659
|
+
declarations: [
|
|
660
|
+
{
|
|
661
|
+
name: "__file",
|
|
662
|
+
initializer: `"${sf.getBaseName()}"`,
|
|
663
|
+
},
|
|
664
|
+
],
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
jobsArr.insertElement(i, namespaceImport);
|
|
668
|
+
|
|
669
|
+
i++;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
generatedFile.addImportDeclarations(imports);
|
|
673
|
+
|
|
674
|
+
await generatedFile.save();
|
|
675
|
+
|
|
676
|
+
return generatedFile;
|
|
746
677
|
}
|
|
747
|
-
}
|
|
748
678
|
}
|
|
749
679
|
|
|
750
680
|
export default TypeScriptCompiler;
|