@flink-app/flink 0.12.1-alpha.2 → 0.12.1-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/FlinkApp.d.ts +5 -2
- package/dist/src/FlinkApp.js +52 -12
- package/dist/src/FlinkHttpHandler.d.ts +14 -1
- package/dist/src/FlinkRepo.d.ts +23 -7
- package/dist/src/FlinkRepo.js +32 -10
- package/dist/src/TypeScriptCompiler.d.ts +10 -0
- package/dist/src/TypeScriptCompiler.js +79 -10
- package/package.json +7 -4
- package/spec/FlinkRepo.spec.ts +11 -0
- package/src/FlinkApp.ts +45 -7
- package/src/FlinkHttpHandler.ts +95 -96
- package/src/FlinkRepo.ts +24 -7
- package/src/TypeScriptCompiler.ts +68 -14
package/dist/src/FlinkApp.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { OptionsJson } from "body-parser";
|
|
2
2
|
import { Express } from "express";
|
|
3
3
|
import { JSONSchema7 } from "json-schema";
|
|
4
|
-
import { Db } from "mongodb";
|
|
4
|
+
import { Db, MongoClient } from "mongodb";
|
|
5
5
|
import { ToadScheduler } from "toad-scheduler";
|
|
6
6
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
7
7
|
import { FlinkContext } from "./FlinkContext";
|
|
@@ -156,6 +156,7 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
156
156
|
name: string;
|
|
157
157
|
expressApp?: Express;
|
|
158
158
|
db?: Db;
|
|
159
|
+
dbClient?: MongoClient;
|
|
159
160
|
handlers: HandlerConfig[];
|
|
160
161
|
port?: number;
|
|
161
162
|
started: boolean;
|
|
@@ -164,13 +165,14 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
164
165
|
private debug;
|
|
165
166
|
private onDbConnection?;
|
|
166
167
|
private plugins;
|
|
167
|
-
|
|
168
|
+
auth?: FlinkAuthPlugin;
|
|
168
169
|
private corsOpts;
|
|
169
170
|
private routingConfigured;
|
|
170
171
|
private jsonOptions?;
|
|
171
172
|
private rawContentTypes?;
|
|
172
173
|
private schedulingOptions?;
|
|
173
174
|
private disableHttpServer;
|
|
175
|
+
private expressServer;
|
|
174
176
|
private repos;
|
|
175
177
|
/**
|
|
176
178
|
* Internal cache used to track registered handlers and potentially any overlapping routes
|
|
@@ -181,6 +183,7 @@ export declare class FlinkApp<C extends FlinkContext> {
|
|
|
181
183
|
constructor(opts: FlinkOptions);
|
|
182
184
|
get ctx(): C;
|
|
183
185
|
start(): Promise<this>;
|
|
186
|
+
stop(): Promise<void>;
|
|
184
187
|
/**
|
|
185
188
|
* Manually registers a handler.
|
|
186
189
|
*
|
package/dist/src/FlinkApp.js
CHANGED
|
@@ -231,7 +231,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
231
231
|
this.started = true;
|
|
232
232
|
}
|
|
233
233
|
else {
|
|
234
|
-
(_d = this.expressApp) === null || _d === void 0 ? void 0 : _d.listen(this.port, function () {
|
|
234
|
+
this.expressServer = (_d = this.expressApp) === null || _d === void 0 ? void 0 : _d.listen(this.port, function () {
|
|
235
235
|
FlinkLog_1.log.fontColorLog("magenta", "\u26A1\uFE0F HTTP server '".concat(_this.name, "' is running and waiting for connections on ").concat(_this.port));
|
|
236
236
|
_this.started = true;
|
|
237
237
|
});
|
|
@@ -241,6 +241,36 @@ var FlinkApp = /** @class */ (function () {
|
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
243
|
};
|
|
244
|
+
FlinkApp.prototype.stop = function () {
|
|
245
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
246
|
+
var _this = this;
|
|
247
|
+
return __generator(this, function (_a) {
|
|
248
|
+
switch (_a.label) {
|
|
249
|
+
case 0:
|
|
250
|
+
FlinkLog_1.log.info("🛑 Stopping Flink app...");
|
|
251
|
+
if (!this.scheduler) return [3 /*break*/, 2];
|
|
252
|
+
return [4 /*yield*/, this.scheduler.stop()];
|
|
253
|
+
case 1:
|
|
254
|
+
_a.sent();
|
|
255
|
+
_a.label = 2;
|
|
256
|
+
case 2:
|
|
257
|
+
if (this.expressServer) {
|
|
258
|
+
return [2 /*return*/, new Promise(function (resolve, reject) {
|
|
259
|
+
var int = setTimeout(function () {
|
|
260
|
+
reject("Failed to stop HTTP server in time");
|
|
261
|
+
}, 2000);
|
|
262
|
+
_this.expressServer.close(function () {
|
|
263
|
+
clearInterval(int);
|
|
264
|
+
FlinkLog_1.log.info("HTTP server stopped");
|
|
265
|
+
resolve();
|
|
266
|
+
});
|
|
267
|
+
})];
|
|
268
|
+
}
|
|
269
|
+
return [2 /*return*/];
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
};
|
|
244
274
|
/**
|
|
245
275
|
* Manually registers a handler.
|
|
246
276
|
*
|
|
@@ -412,11 +442,11 @@ var FlinkApp = /** @class */ (function () {
|
|
|
412
442
|
*/
|
|
413
443
|
FlinkApp.prototype.registerAutoRegisterableHandlers = function () {
|
|
414
444
|
return __awaiter(this, void 0, void 0, function () {
|
|
415
|
-
var _i,
|
|
416
|
-
var
|
|
417
|
-
return __generator(this, function (
|
|
418
|
-
for (_i = 0,
|
|
419
|
-
|
|
445
|
+
var _i, _a, _b, handler, assumedHttpMethod, pathParams, _c, _d, param;
|
|
446
|
+
var _e, _f, _g;
|
|
447
|
+
return __generator(this, function (_h) {
|
|
448
|
+
for (_i = 0, _a = exports.autoRegisteredHandlers.sort(function (a, b) { var _a, _b; return (((_a = a.handler.Route) === null || _a === void 0 ? void 0 : _a.order) || 0) - (((_b = b.handler.Route) === null || _b === void 0 ? void 0 : _b.order) || 0); }); _i < _a.length; _i++) {
|
|
449
|
+
_b = _a[_i], handler = _b.handler, assumedHttpMethod = _b.assumedHttpMethod;
|
|
420
450
|
if (!handler.Route) {
|
|
421
451
|
FlinkLog_1.log.error("Missing Props in handler ".concat(handler.__file));
|
|
422
452
|
continue;
|
|
@@ -425,10 +455,10 @@ var FlinkApp = /** @class */ (function () {
|
|
|
425
455
|
FlinkLog_1.log.error("Missing exported handler function in handler ".concat(handler.__file));
|
|
426
456
|
continue;
|
|
427
457
|
}
|
|
428
|
-
if (!!((
|
|
458
|
+
if (!!((_e = handler.__params) === null || _e === void 0 ? void 0 : _e.length)) {
|
|
429
459
|
pathParams = (0, utils_1.getPathParams)(handler.Route.path);
|
|
430
|
-
for (
|
|
431
|
-
param = _c
|
|
460
|
+
for (_c = 0, _d = handler.__params; _c < _d.length; _c++) {
|
|
461
|
+
param = _d[_c];
|
|
432
462
|
if (!pathParams.includes(param.name)) {
|
|
433
463
|
FlinkLog_1.log.error("Handler ".concat(handler.__file, " has param ").concat(param.name, " but it is not present in the path '").concat(handler.Route.path, "'"));
|
|
434
464
|
throw new Error("Invalid/missing handler path param");
|
|
@@ -441,8 +471,8 @@ var FlinkApp = /** @class */ (function () {
|
|
|
441
471
|
this.registerHandler({
|
|
442
472
|
routeProps: __assign(__assign({}, handler.Route), { method: handler.Route.method || assumedHttpMethod, origin: this.name }),
|
|
443
473
|
schema: {
|
|
444
|
-
reqSchema: (
|
|
445
|
-
resSchema: (
|
|
474
|
+
reqSchema: (_f = handler.__schemas) === null || _f === void 0 ? void 0 : _f.reqSchema,
|
|
475
|
+
resSchema: (_g = handler.__schemas) === null || _g === void 0 ? void 0 : _g.resSchema,
|
|
446
476
|
},
|
|
447
477
|
queryMetadata: handler.__query || [],
|
|
448
478
|
paramsMetadata: handler.__params || [],
|
|
@@ -553,7 +583,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
553
583
|
if (this.dbOpts) {
|
|
554
584
|
for (_i = 0, autoRegisteredRepos_1 = exports.autoRegisteredRepos; _i < autoRegisteredRepos_1.length; _i++) {
|
|
555
585
|
_a = autoRegisteredRepos_1[_i], collectionName = _a.collectionName, repoInstanceName = _a.repoInstanceName, Repo = _a.Repo;
|
|
556
|
-
repoInstance = new Repo(collectionName, this.db);
|
|
586
|
+
repoInstance = new Repo(collectionName, this.db, this.dbClient);
|
|
557
587
|
this.repos[repoInstanceName] = repoInstance;
|
|
558
588
|
FlinkLog_1.log.info("Registered repo ".concat(repoInstanceName));
|
|
559
589
|
}
|
|
@@ -599,6 +629,7 @@ var FlinkApp = /** @class */ (function () {
|
|
|
599
629
|
case 2:
|
|
600
630
|
client = _a.sent();
|
|
601
631
|
this.db = client.db();
|
|
632
|
+
this.dbClient = client;
|
|
602
633
|
return [3 /*break*/, 4];
|
|
603
634
|
case 3:
|
|
604
635
|
err_2 = _a.sent();
|
|
@@ -685,6 +716,15 @@ var FlinkApp = /** @class */ (function () {
|
|
|
685
716
|
if (!this.dbOpts) {
|
|
686
717
|
throw new Error("No db configured");
|
|
687
718
|
}
|
|
719
|
+
var driverVersion = require("mongodb/package.json").version;
|
|
720
|
+
if (driverVersion.startsWith("3")) {
|
|
721
|
+
FlinkLog_1.log.debug("Using legacy mongodb connection options as mongo client is version ".concat(driverVersion));
|
|
722
|
+
return {
|
|
723
|
+
useNewUrlParser: true,
|
|
724
|
+
useUnifiedTopology: true,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
FlinkLog_1.log.debug("Using modern MongoDB client options (driver version ".concat(driverVersion, ")"));
|
|
688
728
|
return {
|
|
689
729
|
serverApi: {
|
|
690
730
|
version: mongodb_1.ServerApiVersion.v1,
|
|
@@ -10,7 +10,12 @@ export declare enum HttpMethod {
|
|
|
10
10
|
delete = "delete"
|
|
11
11
|
}
|
|
12
12
|
type Params = Request["params"];
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Query type for request query parameters.
|
|
15
|
+
* Does currently not allow nested objects, although
|
|
16
|
+
* underlying express Request does allow it.
|
|
17
|
+
*/
|
|
18
|
+
type Query = Record<string, string | string[] | undefined>;
|
|
14
19
|
/**
|
|
15
20
|
* Flink request extends express Request but adds reqId and user object.
|
|
16
21
|
*/
|
|
@@ -63,6 +68,14 @@ export interface RouteProps {
|
|
|
63
68
|
* I.e. filename or plugin name that describes where handler origins from
|
|
64
69
|
*/
|
|
65
70
|
origin?: string;
|
|
71
|
+
/**
|
|
72
|
+
* Order handler should be registered in.
|
|
73
|
+
*
|
|
74
|
+
* By default all handlers has order 0 and in most cases this is fine,
|
|
75
|
+
* but if for example you want to register a handler before all others
|
|
76
|
+
* to avoid conflicts you can set a negative order.
|
|
77
|
+
*/
|
|
78
|
+
order?: number;
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Http handler function that handlers implements in order to
|
package/dist/src/FlinkRepo.d.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { Collection, Db, Document, ObjectId } from "mongodb";
|
|
1
|
+
import { Collection, Db, Document, MongoClient, ObjectId } from "mongodb";
|
|
2
2
|
import { FlinkContext } from "./FlinkContext";
|
|
3
|
+
/**
|
|
4
|
+
* Partial model to have intellisense for partial updates but
|
|
5
|
+
* also allow any other properties to be set such as nested objects etc.
|
|
6
|
+
*/
|
|
7
|
+
type PartialModel<Model> = Partial<Model> & {
|
|
8
|
+
[x: string]: any;
|
|
9
|
+
};
|
|
3
10
|
export declare abstract class FlinkRepo<C extends FlinkContext, Model extends Document> {
|
|
4
|
-
|
|
5
|
-
|
|
11
|
+
collectionName: string;
|
|
12
|
+
db: Db;
|
|
13
|
+
client?: MongoClient | undefined;
|
|
6
14
|
collection: Collection;
|
|
7
15
|
private _ctx?;
|
|
8
16
|
set ctx(ctx: FlinkContext);
|
|
9
17
|
get ctx(): FlinkContext;
|
|
10
|
-
constructor(collectionName: string, db: Db);
|
|
18
|
+
constructor(collectionName: string, db: Db, client?: MongoClient | undefined);
|
|
11
19
|
findAll(query?: {}): Promise<Model[]>;
|
|
12
20
|
getById(id: string | ObjectId): Promise<Model | null>;
|
|
13
21
|
getOne(query?: {}): Promise<Model | null>;
|
|
14
22
|
create<C = Omit<Model, "_id">>(model: C): Promise<C & {
|
|
15
23
|
_id: string;
|
|
16
24
|
}>;
|
|
17
|
-
updateOne(id: string | ObjectId, model:
|
|
18
|
-
updateMany<U =
|
|
25
|
+
updateOne(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null>;
|
|
26
|
+
updateMany<U = PartialModel<Model>>(query: any, model: U): Promise<number>;
|
|
19
27
|
deleteById(id: string | ObjectId): Promise<number>;
|
|
20
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Helper to ensure the id is always an ObjectId.
|
|
30
|
+
* If a string is passed, it will be converted to an ObjectId.
|
|
31
|
+
* If an ObjectId is passed, it will be returned as is.
|
|
32
|
+
* @param id
|
|
33
|
+
* @returns
|
|
34
|
+
*/
|
|
35
|
+
buildId(id: string | ObjectId): ObjectId;
|
|
21
36
|
private objectIdToString;
|
|
22
37
|
}
|
|
38
|
+
export {};
|
package/dist/src/FlinkRepo.js
CHANGED
|
@@ -46,13 +46,25 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
|
46
46
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
50
|
+
var t = {};
|
|
51
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
52
|
+
t[p] = s[p];
|
|
53
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
54
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
55
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
56
|
+
t[p[i]] = s[p[i]];
|
|
57
|
+
}
|
|
58
|
+
return t;
|
|
59
|
+
};
|
|
49
60
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
61
|
exports.FlinkRepo = void 0;
|
|
51
62
|
var mongodb_1 = require("mongodb");
|
|
52
63
|
var FlinkRepo = /** @class */ (function () {
|
|
53
|
-
function FlinkRepo(collectionName, db) {
|
|
64
|
+
function FlinkRepo(collectionName, db, client) {
|
|
54
65
|
this.collectionName = collectionName;
|
|
55
66
|
this.db = db;
|
|
67
|
+
this.client = client;
|
|
56
68
|
this.collection = db.collection(this.collectionName);
|
|
57
69
|
}
|
|
58
70
|
Object.defineProperty(FlinkRepo.prototype, "ctx", {
|
|
@@ -129,12 +141,13 @@ var FlinkRepo = /** @class */ (function () {
|
|
|
129
141
|
};
|
|
130
142
|
FlinkRepo.prototype.updateOne = function (id, model) {
|
|
131
143
|
return __awaiter(this, void 0, void 0, function () {
|
|
132
|
-
var oid, res;
|
|
144
|
+
var oid, _id, modelWithoutId, res;
|
|
133
145
|
return __generator(this, function (_a) {
|
|
134
146
|
switch (_a.label) {
|
|
135
147
|
case 0:
|
|
136
148
|
oid = this.buildId(id);
|
|
137
|
-
|
|
149
|
+
_id = model._id, modelWithoutId = __rest(model, ["_id"]);
|
|
150
|
+
return [4 /*yield*/, this.collection.updateOne({ _id: oid }, { $set: modelWithoutId })];
|
|
138
151
|
case 1:
|
|
139
152
|
_a.sent();
|
|
140
153
|
return [4 /*yield*/, this.collection.findOne({ _id: oid })];
|
|
@@ -150,14 +163,16 @@ var FlinkRepo = /** @class */ (function () {
|
|
|
150
163
|
};
|
|
151
164
|
FlinkRepo.prototype.updateMany = function (query, model) {
|
|
152
165
|
return __awaiter(this, void 0, void 0, function () {
|
|
153
|
-
var modifiedCount;
|
|
154
|
-
return __generator(this, function (
|
|
155
|
-
switch (
|
|
156
|
-
case 0:
|
|
157
|
-
|
|
158
|
-
|
|
166
|
+
var _a, _id, modelWithoutId, modifiedCount;
|
|
167
|
+
return __generator(this, function (_b) {
|
|
168
|
+
switch (_b.label) {
|
|
169
|
+
case 0:
|
|
170
|
+
_a = model, _id = _a._id, modelWithoutId = __rest(_a, ["_id"]);
|
|
171
|
+
return [4 /*yield*/, this.collection.updateMany(query, {
|
|
172
|
+
$set: modelWithoutId,
|
|
173
|
+
})];
|
|
159
174
|
case 1:
|
|
160
|
-
modifiedCount = (
|
|
175
|
+
modifiedCount = (_b.sent()).modifiedCount;
|
|
161
176
|
return [2 /*return*/, modifiedCount];
|
|
162
177
|
}
|
|
163
178
|
});
|
|
@@ -178,6 +193,13 @@ var FlinkRepo = /** @class */ (function () {
|
|
|
178
193
|
});
|
|
179
194
|
});
|
|
180
195
|
};
|
|
196
|
+
/**
|
|
197
|
+
* Helper to ensure the id is always an ObjectId.
|
|
198
|
+
* If a string is passed, it will be converted to an ObjectId.
|
|
199
|
+
* If an ObjectId is passed, it will be returned as is.
|
|
200
|
+
* @param id
|
|
201
|
+
* @returns
|
|
202
|
+
*/
|
|
181
203
|
FlinkRepo.prototype.buildId = function (id) {
|
|
182
204
|
var oid;
|
|
183
205
|
if (typeof id === "string") {
|
|
@@ -3,6 +3,7 @@ declare class TypeScriptCompiler {
|
|
|
3
3
|
private cwd;
|
|
4
4
|
private project;
|
|
5
5
|
private schemaGenerator?;
|
|
6
|
+
private isEsm;
|
|
6
7
|
/**
|
|
7
8
|
* Parsed typescript schemas that will be added to intermediate
|
|
8
9
|
* schemas.ts file.
|
|
@@ -17,6 +18,15 @@ declare class TypeScriptCompiler {
|
|
|
17
18
|
*/
|
|
18
19
|
private tsSchemasSymbolsToImports;
|
|
19
20
|
constructor(cwd: string);
|
|
21
|
+
/**
|
|
22
|
+
* Detects if the project is using ESM (ECMAScript Modules)
|
|
23
|
+
* by checking type in package.json.
|
|
24
|
+
*/
|
|
25
|
+
private isEsmProject;
|
|
26
|
+
/**
|
|
27
|
+
* Gets the module specifier for imports, adding .js extension for ESM
|
|
28
|
+
*/
|
|
29
|
+
private getModuleSpecifier;
|
|
20
30
|
/**
|
|
21
31
|
* Deletes all generated files.
|
|
22
32
|
* @param cwd
|
|
@@ -10,6 +10,29 @@ var __assign = (this && this.__assign) || function () {
|
|
|
10
10
|
};
|
|
11
11
|
return __assign.apply(this, arguments);
|
|
12
12
|
};
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
30
|
+
if (mod && mod.__esModule) return mod;
|
|
31
|
+
var result = {};
|
|
32
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
33
|
+
__setModuleDefault(result, mod);
|
|
34
|
+
return result;
|
|
35
|
+
};
|
|
13
36
|
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
37
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
38
|
return new (P || (P = Promise))(function (resolve, reject) {
|
|
@@ -59,7 +82,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
59
82
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
60
83
|
};
|
|
61
84
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
62
|
-
var fs_1 = require("fs");
|
|
85
|
+
var fs_1 = __importStar(require("fs"));
|
|
63
86
|
var path_1 = require("path");
|
|
64
87
|
var tiny_glob_1 = __importDefault(require("tiny-glob"));
|
|
65
88
|
var ts_json_schema_generator_1 = require("ts-json-schema-generator");
|
|
@@ -83,16 +106,62 @@ var TypeScriptCompiler = /** @class */ (function () {
|
|
|
83
106
|
* This will be added to file in a batch for performance reasons.
|
|
84
107
|
*/
|
|
85
108
|
this.tsSchemasSymbolsToImports = [];
|
|
109
|
+
// Detect if project is using ESM based solely on package.json "type": "module"
|
|
110
|
+
this.isEsm = this.isEsmProject(cwd);
|
|
111
|
+
var compilerOptions = {
|
|
112
|
+
noEmit: false, // We need to emit files
|
|
113
|
+
outDir: (0, path_1.join)(cwd, "dist"),
|
|
114
|
+
};
|
|
115
|
+
// Set appropriate module settings based on detected module system
|
|
116
|
+
if (this.isEsm) {
|
|
117
|
+
// For ESM projects, use ESNext module with Node resolution
|
|
118
|
+
compilerOptions.module = ts_morph_1.ts.ModuleKind.ESNext;
|
|
119
|
+
compilerOptions.moduleResolution = ts_morph_1.ts.ModuleResolutionKind.NodeJs;
|
|
120
|
+
compilerOptions.esModuleInterop = true;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// For CommonJS projects, use CommonJS module with Node resolution
|
|
124
|
+
compilerOptions.module = ts_morph_1.ts.ModuleKind.CommonJS;
|
|
125
|
+
compilerOptions.moduleResolution = ts_morph_1.ts.ModuleResolutionKind.NodeJs;
|
|
126
|
+
}
|
|
86
127
|
this.project = new ts_morph_1.Project({
|
|
87
128
|
tsConfigFilePath: (0, path_1.join)(cwd, "tsconfig.json"),
|
|
88
|
-
compilerOptions:
|
|
89
|
-
noEmit: false,
|
|
90
|
-
outDir: (0, path_1.join)(cwd, "dist"),
|
|
91
|
-
// incremental: true,
|
|
92
|
-
},
|
|
129
|
+
compilerOptions: compilerOptions,
|
|
93
130
|
});
|
|
94
131
|
console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
|
|
132
|
+
console.log("Module system:", this.isEsm ? "ESM" : "CommonJS");
|
|
133
|
+
console.log("Using module:", compilerOptions.module === ts_morph_1.ts.ModuleKind.ESNext ? "ESNext" : "CommonJS");
|
|
95
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Detects if the project is using ESM (ECMAScript Modules)
|
|
137
|
+
* by checking type in package.json.
|
|
138
|
+
*/
|
|
139
|
+
TypeScriptCompiler.prototype.isEsmProject = function (cwd) {
|
|
140
|
+
try {
|
|
141
|
+
// Check package.json for "type": "module"
|
|
142
|
+
var packageJsonPath = (0, path_1.join)(cwd, "package.json");
|
|
143
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
144
|
+
var packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
145
|
+
return packageJson.type === "module";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// If we can't determine, default to CommonJS
|
|
150
|
+
console.warn("Error detecting module system, defaulting to CommonJS:", error);
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Gets the module specifier for imports, adding .js extension for ESM
|
|
156
|
+
*/
|
|
157
|
+
TypeScriptCompiler.prototype.getModuleSpecifier = function (fromFile, toFile) {
|
|
158
|
+
var moduleSpecifier = fromFile.getRelativePathAsModuleSpecifierTo(toFile);
|
|
159
|
+
// Add .js extension for ESM imports (only for relative paths)
|
|
160
|
+
if (this.isEsm && !moduleSpecifier.startsWith("@") && !moduleSpecifier.endsWith(".js")) {
|
|
161
|
+
moduleSpecifier += ".js";
|
|
162
|
+
}
|
|
163
|
+
return moduleSpecifier;
|
|
164
|
+
};
|
|
96
165
|
/**
|
|
97
166
|
* Deletes all generated files.
|
|
98
167
|
* @param cwd
|
|
@@ -233,7 +302,7 @@ var TypeScriptCompiler = /** @class */ (function () {
|
|
|
233
302
|
namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
234
303
|
imports.push({
|
|
235
304
|
defaultImport: "* as " + namespaceImport,
|
|
236
|
-
moduleSpecifier:
|
|
305
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
237
306
|
});
|
|
238
307
|
assumedHttpMethod = (0, utils_1.getHttpMethodFromHandlerName)(sf.getBaseName());
|
|
239
308
|
return [4 /*yield*/, this.extractSchemasFromHandlerSourceFile(sf)];
|
|
@@ -309,7 +378,7 @@ var TypeScriptCompiler = /** @class */ (function () {
|
|
|
309
378
|
console.log("Detected repo ".concat(sf.getBaseName()));
|
|
310
379
|
imports.push({
|
|
311
380
|
defaultImport: sf.getBaseNameWithoutExtension(),
|
|
312
|
-
moduleSpecifier:
|
|
381
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
313
382
|
});
|
|
314
383
|
reposArr.insertElement(i, "{collectionName: \"".concat((0, utils_1.getCollectionNameForRepo)(sf.getBaseName()), "\", repoInstanceName: \"").concat((0, utils_1.getRepoInstanceName)(sf.getBaseName()), "\", Repo: ").concat(sf.getBaseNameWithoutExtension(), "}"));
|
|
315
384
|
i++;
|
|
@@ -341,7 +410,7 @@ var TypeScriptCompiler = /** @class */ (function () {
|
|
|
341
410
|
console.error("Cannot find entry script '".concat(appEntryScript, "'"));
|
|
342
411
|
return [2 /*return*/, process.exit(1)];
|
|
343
412
|
}
|
|
344
|
-
sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers\";\nimport \"./generatedRepos\";\nimport \"./generatedJobs\";\nimport \"..").concat(appEntryScript.replace(/\.ts/g, ""), "\";\n"));
|
|
413
|
+
sf = this.createSourceFile(["start.ts"], "// Generated ".concat(new Date(), "\nimport \"./generatedHandlers").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedRepos").concat(this.isEsm ? ".js" : "", "\";\nimport \"./generatedJobs").concat(this.isEsm ? ".js" : "", "\";\nimport \"..").concat(appEntryScript.replace(/\.ts/g, "")).concat(this.isEsm ? ".js" : "", "\";\nexport default {}; // Export an empty object to make it a module\n"));
|
|
345
414
|
return [4 /*yield*/, sf.save()];
|
|
346
415
|
case 1:
|
|
347
416
|
_a.sent();
|
|
@@ -672,7 +741,7 @@ var TypeScriptCompiler = /** @class */ (function () {
|
|
|
672
741
|
namespaceImport = sf.getBaseNameWithoutExtension().replace(/\./g, "_") + "_" + i;
|
|
673
742
|
imports.push({
|
|
674
743
|
defaultImport: "* as " + namespaceImport,
|
|
675
|
-
moduleSpecifier:
|
|
744
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
676
745
|
});
|
|
677
746
|
// Append metadata to source file that will be part of emitted dist bundle (javascript)
|
|
678
747
|
sf.addVariableStatement({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flink-app/flink",
|
|
3
|
-
"version": "0.12.1-alpha.
|
|
3
|
+
"version": "0.12.1-alpha.20",
|
|
4
4
|
"description": "Typescript only framework for creating REST-like APIs on top of Express and mongodb",
|
|
5
5
|
"types": "dist/src/index.d.ts",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"test": "jasmine-ts --preserve-symlinks --config=./spec/support/jasmine.json",
|
|
10
10
|
"test:watch": "nodemon --ext ts --exec 'jasmine-ts --config=./spec/support/jasmine.json'",
|
|
11
11
|
"start": "ts-node src/index.ts",
|
|
12
|
-
"
|
|
12
|
+
"prepare": "npm run build",
|
|
13
13
|
"build": "tsc --project tsconfig.dist.json",
|
|
14
14
|
"watch": "nodemon --exec 'tsc --project tsconfig.dist.json'",
|
|
15
15
|
"clean": "rimraf dist"
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"fs-extra": "^10.0.0",
|
|
39
39
|
"mkdirp": "^1.0.4",
|
|
40
40
|
"mock-json-schema": "^1.0.8",
|
|
41
|
-
"mongodb": "^6.15.0",
|
|
42
41
|
"morgan": "^1.10.0",
|
|
43
42
|
"ms": "^2.0.0",
|
|
44
43
|
"node-color-log": "^10.0.2",
|
|
@@ -62,9 +61,13 @@
|
|
|
62
61
|
"jasmine": "^3.7.0",
|
|
63
62
|
"jasmine-spec-reporter": "^7.0.0",
|
|
64
63
|
"jasmine-ts": "^0.3.3",
|
|
64
|
+
"mongodb": "^6.15.0",
|
|
65
65
|
"nodemon": "^2.0.7",
|
|
66
66
|
"rimraf": "^3.0.2",
|
|
67
67
|
"ts-node": "^9.1.1"
|
|
68
68
|
},
|
|
69
|
-
"
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"mongodb": ">=3.7.0 <7.0.0"
|
|
71
|
+
},
|
|
72
|
+
"gitHead": "133c6f50b3119f8264f6350e2043c171781b583f"
|
|
70
73
|
}
|
package/spec/FlinkRepo.spec.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { FlinkRepo } from "../src/FlinkRepo";
|
|
|
4
4
|
interface Model {
|
|
5
5
|
_id: string;
|
|
6
6
|
name: string;
|
|
7
|
+
nested?: { field: number };
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
class Repo extends FlinkRepo<any, Model> {}
|
|
@@ -61,9 +62,19 @@ describe("FlinkRepo", () => {
|
|
|
61
62
|
|
|
62
63
|
const updatedDoc = await repo.updateOne(createdDoc._id + "", {
|
|
63
64
|
name: "foo",
|
|
65
|
+
"nested.field": 1,
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
expect(updatedDoc).toBeDefined();
|
|
67
69
|
expect(updatedDoc?.name).toBe("foo");
|
|
70
|
+
expect(updatedDoc?.nested?.field).toBe(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should update many documents", async () => {
|
|
74
|
+
await collection.insertMany([{ name: "foo" }, { name: "foo" }, { name: "foo" }]);
|
|
75
|
+
|
|
76
|
+
const updatedCount = await repo.updateMany({ name: "foo" }, { name: "bar", "nested.field": 1 });
|
|
77
|
+
|
|
78
|
+
expect(updatedCount).toBe(3);
|
|
68
79
|
});
|
|
69
80
|
});
|
package/src/FlinkApp.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { v4 } from "uuid";
|
|
|
12
12
|
import { FlinkAuthPlugin } from "./auth/FlinkAuthPlugin";
|
|
13
13
|
import { FlinkContext } from "./FlinkContext";
|
|
14
14
|
import { internalServerError, notFound, unauthorized } from "./FlinkErrors";
|
|
15
|
-
import { Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
15
|
+
import { FlinkRequest, Handler, HandlerFile, HttpMethod, QueryParamMetadata, RouteProps } from "./FlinkHttpHandler";
|
|
16
16
|
import { FlinkJobFile } from "./FlinkJob";
|
|
17
17
|
import { log } from "./FlinkLog";
|
|
18
18
|
import { FlinkPlugin } from "./FlinkPlugin";
|
|
@@ -217,6 +217,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
217
217
|
public name: string;
|
|
218
218
|
public expressApp?: Express;
|
|
219
219
|
public db?: Db;
|
|
220
|
+
public dbClient?: MongoClient;
|
|
220
221
|
public handlers: HandlerConfig[] = [];
|
|
221
222
|
public port?: number;
|
|
222
223
|
public started = false;
|
|
@@ -227,13 +228,14 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
227
228
|
private onDbConnection?: FlinkOptions["onDbConnection"];
|
|
228
229
|
|
|
229
230
|
private plugins: FlinkPlugin[] = [];
|
|
230
|
-
|
|
231
|
+
public auth?: FlinkAuthPlugin;
|
|
231
232
|
private corsOpts: FlinkOptions["cors"];
|
|
232
233
|
private routingConfigured = false;
|
|
233
234
|
private jsonOptions?: OptionsJson;
|
|
234
235
|
private rawContentTypes?: string[];
|
|
235
236
|
private schedulingOptions?: FlinkOptions["scheduling"];
|
|
236
237
|
private disableHttpServer = false;
|
|
238
|
+
private expressServer: any; // for simplicity, we don't want to import types from express/node here
|
|
237
239
|
|
|
238
240
|
private repos: { [x: string]: FlinkRepo<C, any> } = {};
|
|
239
241
|
|
|
@@ -367,7 +369,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
367
369
|
log.info("🚧 HTTP server is disabled, but flink app is running");
|
|
368
370
|
this.started = true;
|
|
369
371
|
} else {
|
|
370
|
-
this.expressApp?.listen(this.port, () => {
|
|
372
|
+
this.expressServer = this.expressApp?.listen(this.port, () => {
|
|
371
373
|
log.fontColorLog("magenta", `⚡️ HTTP server '${this.name}' is running and waiting for connections on ${this.port}`);
|
|
372
374
|
this.started = true;
|
|
373
375
|
});
|
|
@@ -376,6 +378,28 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
376
378
|
return this;
|
|
377
379
|
}
|
|
378
380
|
|
|
381
|
+
async stop() {
|
|
382
|
+
log.info("🛑 Stopping Flink app...");
|
|
383
|
+
|
|
384
|
+
if (this.scheduler) {
|
|
385
|
+
await this.scheduler.stop();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (this.expressServer) {
|
|
389
|
+
return new Promise<void>((resolve, reject) => {
|
|
390
|
+
const int = setTimeout(() => {
|
|
391
|
+
reject("Failed to stop HTTP server in time");
|
|
392
|
+
}, 2000);
|
|
393
|
+
|
|
394
|
+
this.expressServer.close(() => {
|
|
395
|
+
clearInterval(int);
|
|
396
|
+
log.info("HTTP server stopped");
|
|
397
|
+
resolve();
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
379
403
|
/**
|
|
380
404
|
* Manually registers a handler.
|
|
381
405
|
*
|
|
@@ -506,7 +530,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
506
530
|
try {
|
|
507
531
|
// 👇 This is where the actual handler gets invoked
|
|
508
532
|
handlerRes = await handler({
|
|
509
|
-
req,
|
|
533
|
+
req: req as FlinkRequest,
|
|
510
534
|
ctx: this.ctx,
|
|
511
535
|
origin: routeProps.origin,
|
|
512
536
|
});
|
|
@@ -570,7 +594,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
570
594
|
* Will not register any handlers added programmatically.
|
|
571
595
|
*/
|
|
572
596
|
private async registerAutoRegisterableHandlers() {
|
|
573
|
-
for (const { handler, assumedHttpMethod } of autoRegisteredHandlers) {
|
|
597
|
+
for (const { handler, assumedHttpMethod } of autoRegisteredHandlers.sort((a, b) => (a.handler.Route?.order || 0) - (b.handler.Route?.order || 0))) {
|
|
574
598
|
if (!handler.Route) {
|
|
575
599
|
log.error(`Missing Props in handler ${handler.__file}`);
|
|
576
600
|
continue;
|
|
@@ -717,7 +741,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
717
741
|
private async buildContext() {
|
|
718
742
|
if (this.dbOpts) {
|
|
719
743
|
for (const { collectionName, repoInstanceName, Repo } of autoRegisteredRepos) {
|
|
720
|
-
const repoInstance: FlinkRepo<C, any> = new Repo(collectionName, this.db);
|
|
744
|
+
const repoInstance: FlinkRepo<C, any> = new Repo(collectionName, this.db, this.dbClient);
|
|
721
745
|
|
|
722
746
|
this.repos[repoInstanceName] = repoInstance;
|
|
723
747
|
|
|
@@ -753,8 +777,10 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
753
777
|
if (this.dbOpts) {
|
|
754
778
|
try {
|
|
755
779
|
log.debug("Connecting to db");
|
|
780
|
+
|
|
756
781
|
const client = await MongoClient.connect(this.dbOpts.uri, this.getMongoConnectionOptions());
|
|
757
782
|
this.db = client.db();
|
|
783
|
+
this.dbClient = client;
|
|
758
784
|
} catch (err) {
|
|
759
785
|
log.error("Failed to connect to db: " + err);
|
|
760
786
|
process.exit(1);
|
|
@@ -797,7 +823,7 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
797
823
|
if (!this.auth) {
|
|
798
824
|
throw new Error(`Attempting to authenticate request (${req.method} ${req.path}) but no authPlugin is set`);
|
|
799
825
|
}
|
|
800
|
-
return await this.auth.authenticateRequest(req, permissions);
|
|
826
|
+
return await this.auth.authenticateRequest(req as FlinkRequest, permissions);
|
|
801
827
|
}
|
|
802
828
|
|
|
803
829
|
public getRegisteredRoutes() {
|
|
@@ -813,6 +839,18 @@ export class FlinkApp<C extends FlinkContext> {
|
|
|
813
839
|
throw new Error("No db configured");
|
|
814
840
|
}
|
|
815
841
|
|
|
842
|
+
const { version: driverVersion } = require("mongodb/package.json");
|
|
843
|
+
|
|
844
|
+
if (driverVersion.startsWith("3")) {
|
|
845
|
+
log.debug(`Using legacy mongodb connection options as mongo client is version ${driverVersion}`);
|
|
846
|
+
return {
|
|
847
|
+
useNewUrlParser: true,
|
|
848
|
+
useUnifiedTopology: true,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
log.debug(`Using modern MongoDB client options (driver version ${driverVersion})`);
|
|
853
|
+
|
|
816
854
|
return {
|
|
817
855
|
serverApi: {
|
|
818
856
|
version: ServerApiVersion.v1,
|
package/src/FlinkHttpHandler.ts
CHANGED
|
@@ -5,24 +5,25 @@ import { FlinkError } from "./FlinkErrors";
|
|
|
5
5
|
import { FlinkResponse } from "./FlinkResponse";
|
|
6
6
|
|
|
7
7
|
export enum HttpMethod {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
get = "get",
|
|
9
|
+
post = "post",
|
|
10
|
+
put = "put",
|
|
11
|
+
delete = "delete",
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
type Params = Request["params"];
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Query type for request query parameters.
|
|
18
|
+
* Does currently not allow nested objects, although
|
|
19
|
+
* underlying express Request does allow it.
|
|
20
|
+
*/
|
|
21
|
+
type Query = Record<string, string | string[] | undefined>;
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Flink request extends express Request but adds reqId and user object.
|
|
19
25
|
*/
|
|
20
|
-
export type FlinkRequest<T = any, P = Params, Q = Query> = Request<
|
|
21
|
-
P,
|
|
22
|
-
any,
|
|
23
|
-
T,
|
|
24
|
-
Q
|
|
25
|
-
> & { reqId: string; user?: any };
|
|
26
|
+
export type FlinkRequest<T = any, P = Params, Q = Query> = Request<P, any, T, Q> & { reqId: string; user?: any };
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* Route props to control routing.
|
|
@@ -31,66 +32,69 @@ export type FlinkRequest<T = any, P = Params, Q = Query> = Request<
|
|
|
31
32
|
* instructs express web server how to route traffic.
|
|
32
33
|
*/
|
|
33
34
|
export interface RouteProps {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
35
|
+
/**
|
|
36
|
+
* HTTP method which this handlers responds to.
|
|
37
|
+
*
|
|
38
|
+
* Will if not set attempt to extract HTTP method based
|
|
39
|
+
* on handler file name prefix, for example `GetFoo.ts` will assume
|
|
40
|
+
* HTTP method `GET`.
|
|
41
|
+
*/
|
|
42
|
+
method?: HttpMethod;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Route path including any path params.
|
|
46
|
+
* Example: `/user/:id`
|
|
47
|
+
*/
|
|
48
|
+
path: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generates mock response based on handlers response schema.
|
|
52
|
+
*
|
|
53
|
+
* Will be ignored if handler does not have any response schema defined.
|
|
54
|
+
*
|
|
55
|
+
* This should only be used during development 💥
|
|
56
|
+
*/
|
|
57
|
+
mockApi?: boolean;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set permissions needed to access route if route requires authentication.
|
|
61
|
+
*/
|
|
62
|
+
permissions?: string | string[];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Optional documentation of endpoint. Can be used for example in API docs.
|
|
66
|
+
* Supports markdown strings.
|
|
67
|
+
*/
|
|
68
|
+
docs?: string; // TODO
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* If handler should not be auto registered
|
|
72
|
+
*/
|
|
73
|
+
skipAutoRegister?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* I.e. filename or plugin name that describes where handler origins from
|
|
77
|
+
*/
|
|
78
|
+
origin?: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Order handler should be registered in.
|
|
82
|
+
*
|
|
83
|
+
* By default all handlers has order 0 and in most cases this is fine,
|
|
84
|
+
* but if for example you want to register a handler before all others
|
|
85
|
+
* to avoid conflicts you can set a negative order.
|
|
86
|
+
*/
|
|
87
|
+
order?: number;
|
|
78
88
|
}
|
|
79
89
|
|
|
80
90
|
/**
|
|
81
91
|
* Http handler function that handlers implements in order to
|
|
82
92
|
* handle HTTP requests and return a JSON response.
|
|
83
93
|
*/
|
|
84
|
-
export type Handler<
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
P extends Params = Params,
|
|
89
|
-
Q extends Query = Query
|
|
90
|
-
> = (props: {
|
|
91
|
-
req: FlinkRequest<ReqSchema, P, Q>;
|
|
92
|
-
ctx: Ctx;
|
|
93
|
-
origin?: string;
|
|
94
|
+
export type Handler<Ctx extends FlinkContext, ReqSchema = any, ResSchema = any, P extends Params = Params, Q extends Query = Query> = (props: {
|
|
95
|
+
req: FlinkRequest<ReqSchema, P, Q>;
|
|
96
|
+
ctx: Ctx;
|
|
97
|
+
origin?: string;
|
|
94
98
|
}) => Promise<FlinkResponse<ResSchema | FlinkError>>;
|
|
95
99
|
|
|
96
100
|
/**
|
|
@@ -99,12 +103,7 @@ export type Handler<
|
|
|
99
103
|
*
|
|
100
104
|
* Just syntactic sugar on top op `HandlerFn`
|
|
101
105
|
*/
|
|
102
|
-
export type GetHandler<
|
|
103
|
-
Ctx extends FlinkContext,
|
|
104
|
-
ResSchema = any,
|
|
105
|
-
P extends Params = Params,
|
|
106
|
-
Q extends Query = Query
|
|
107
|
-
> = Handler<Ctx, any, ResSchema, P, Q>;
|
|
106
|
+
export type GetHandler<Ctx extends FlinkContext, ResSchema = any, P extends Params = Params, Q extends Query = Query> = Handler<Ctx, any, ResSchema, P, Q>;
|
|
108
107
|
|
|
109
108
|
/**
|
|
110
109
|
* Type for Handler file. Describes shape of exports when using
|
|
@@ -113,32 +112,32 @@ export type GetHandler<
|
|
|
113
112
|
* `import * as FooHandler from "./src/handlers/FooHandler"
|
|
114
113
|
*/
|
|
115
114
|
export type HandlerFile = {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
115
|
+
default: Handler<any, any, any, any, any>;
|
|
116
|
+
Route?: RouteProps;
|
|
117
|
+
/**
|
|
118
|
+
* Name of schemas, is set at compile time by Flink compiler.
|
|
119
|
+
*/
|
|
120
|
+
__schemas?: {
|
|
121
|
+
reqSchema?: JSONSchema;
|
|
122
|
+
resSchema?: JSONSchema;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Typescript source file name, is set at compile time by Flink compiler.
|
|
126
|
+
*/
|
|
127
|
+
__file?: string;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Description of query params, is set at compile time by Flink compiler.
|
|
131
|
+
*/
|
|
132
|
+
__query?: QueryParamMetadata[];
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Description of path params, is set at compile time by Flink compiler.
|
|
136
|
+
*/
|
|
137
|
+
__params?: QueryParamMetadata[];
|
|
139
138
|
};
|
|
140
139
|
|
|
141
140
|
export type QueryParamMetadata = {
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
name: string;
|
|
142
|
+
description: string;
|
|
144
143
|
};
|
package/src/FlinkRepo.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { Collection, Db, Document, InsertOneResult, ObjectId } from "mongodb";
|
|
1
|
+
import { Collection, Db, Document, InsertOneResult, MongoClient, ObjectId } from "mongodb";
|
|
2
2
|
import { FlinkContext } from "./FlinkContext";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Partial model to have intellisense for partial updates but
|
|
6
|
+
* also allow any other properties to be set such as nested objects etc.
|
|
7
|
+
*/
|
|
8
|
+
type PartialModel<Model> = Partial<Model> & { [x: string]: any };
|
|
9
|
+
|
|
4
10
|
export abstract class FlinkRepo<C extends FlinkContext, Model extends Document> {
|
|
5
11
|
collection: Collection;
|
|
6
12
|
|
|
@@ -15,7 +21,7 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
15
21
|
return this._ctx;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
constructor(
|
|
24
|
+
constructor(public collectionName: string, public db: Db, public client?: MongoClient) {
|
|
19
25
|
this.collection = db.collection(this.collectionName);
|
|
20
26
|
}
|
|
21
27
|
|
|
@@ -45,10 +51,12 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
45
51
|
return { ...model, _id: result.insertedId.toString() };
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
async updateOne(id: string | ObjectId, model:
|
|
54
|
+
async updateOne(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null> {
|
|
49
55
|
const oid = this.buildId(id);
|
|
50
56
|
|
|
51
|
-
|
|
57
|
+
const { _id, ...modelWithoutId } = model;
|
|
58
|
+
|
|
59
|
+
await this.collection.updateOne({ _id: oid }, { $set: modelWithoutId });
|
|
52
60
|
|
|
53
61
|
const res = await this.collection.findOne<Model>({ _id: oid });
|
|
54
62
|
|
|
@@ -58,9 +66,11 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
58
66
|
return null;
|
|
59
67
|
}
|
|
60
68
|
|
|
61
|
-
async updateMany<U =
|
|
69
|
+
async updateMany<U = PartialModel<Model>>(query: any, model: U): Promise<number> {
|
|
70
|
+
const { _id, ...modelWithoutId } = model as any;
|
|
71
|
+
|
|
62
72
|
const { modifiedCount } = await this.collection.updateMany(query, {
|
|
63
|
-
$set:
|
|
73
|
+
$set: modelWithoutId as any,
|
|
64
74
|
});
|
|
65
75
|
return modifiedCount;
|
|
66
76
|
}
|
|
@@ -72,7 +82,14 @@ export abstract class FlinkRepo<C extends FlinkContext, Model extends Document>
|
|
|
72
82
|
return deletedCount || 0;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Helper to ensure the id is always an ObjectId.
|
|
87
|
+
* If a string is passed, it will be converted to an ObjectId.
|
|
88
|
+
* If an ObjectId is passed, it will be returned as is.
|
|
89
|
+
* @param id
|
|
90
|
+
* @returns
|
|
91
|
+
*/
|
|
92
|
+
buildId(id: string | ObjectId) {
|
|
76
93
|
let oid: ObjectId | string;
|
|
77
94
|
|
|
78
95
|
if (typeof id === "string") {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { promises as fsPromises } from "fs";
|
|
1
|
+
import fs, { 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 { CompletedConfig,
|
|
5
|
+
import { CompletedConfig, createFormatter, createParser, Schema, SchemaGenerator } from "ts-json-schema-generator";
|
|
6
6
|
import {
|
|
7
7
|
ArrayLiteralExpression,
|
|
8
8
|
DiagnosticCategory,
|
|
@@ -24,6 +24,7 @@ import { getCollectionNameForRepo, getHttpMethodFromHandlerName, getRepoInstance
|
|
|
24
24
|
class TypeScriptCompiler {
|
|
25
25
|
private project: Project;
|
|
26
26
|
private schemaGenerator?: SchemaGenerator;
|
|
27
|
+
private isEsm: boolean;
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Parsed typescript schemas that will be added to intermediate
|
|
@@ -41,16 +42,68 @@ class TypeScriptCompiler {
|
|
|
41
42
|
private tsSchemasSymbolsToImports: Symbol[] = [];
|
|
42
43
|
|
|
43
44
|
constructor(private cwd: string) {
|
|
45
|
+
// Detect if project is using ESM based solely on package.json "type": "module"
|
|
46
|
+
this.isEsm = this.isEsmProject(cwd);
|
|
47
|
+
|
|
48
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
49
|
+
noEmit: false, // We need to emit files
|
|
50
|
+
outDir: join(cwd, "dist"),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Set appropriate module settings based on detected module system
|
|
54
|
+
if (this.isEsm) {
|
|
55
|
+
// For ESM projects, use ESNext module with Node resolution
|
|
56
|
+
compilerOptions.module = ts.ModuleKind.ESNext;
|
|
57
|
+
compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs;
|
|
58
|
+
compilerOptions.esModuleInterop = true;
|
|
59
|
+
} else {
|
|
60
|
+
// For CommonJS projects, use CommonJS module with Node resolution
|
|
61
|
+
compilerOptions.module = ts.ModuleKind.CommonJS;
|
|
62
|
+
compilerOptions.moduleResolution = ts.ModuleResolutionKind.NodeJs;
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
this.project = new Project({
|
|
45
66
|
tsConfigFilePath: join(cwd, "tsconfig.json"),
|
|
46
|
-
compilerOptions
|
|
47
|
-
noEmit: false,
|
|
48
|
-
outDir: join(cwd, "dist"),
|
|
49
|
-
// incremental: true,
|
|
50
|
-
},
|
|
67
|
+
compilerOptions,
|
|
51
68
|
});
|
|
52
69
|
|
|
53
70
|
console.log("Loaded", this.project.getSourceFiles().length, "source file(s) from", cwd);
|
|
71
|
+
console.log("Module system:", this.isEsm ? "ESM" : "CommonJS");
|
|
72
|
+
console.log("Using module:", compilerOptions.module === ts.ModuleKind.ESNext ? "ESNext" : "CommonJS");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detects if the project is using ESM (ECMAScript Modules)
|
|
77
|
+
* by checking type in package.json.
|
|
78
|
+
*/
|
|
79
|
+
private isEsmProject(cwd: string): boolean {
|
|
80
|
+
try {
|
|
81
|
+
// Check package.json for "type": "module"
|
|
82
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
83
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
84
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
85
|
+
return packageJson.type === "module";
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
// If we can't determine, default to CommonJS
|
|
89
|
+
console.warn("Error detecting module system, defaulting to CommonJS:", error);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gets the module specifier for imports, adding .js extension for ESM
|
|
97
|
+
*/
|
|
98
|
+
private getModuleSpecifier(fromFile: SourceFile, toFile: SourceFile): string {
|
|
99
|
+
let moduleSpecifier = fromFile.getRelativePathAsModuleSpecifierTo(toFile);
|
|
100
|
+
|
|
101
|
+
// Add .js extension for ESM imports (only for relative paths)
|
|
102
|
+
if (this.isEsm && !moduleSpecifier.startsWith("@") && !moduleSpecifier.endsWith(".js")) {
|
|
103
|
+
moduleSpecifier += ".js";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return moduleSpecifier;
|
|
54
107
|
}
|
|
55
108
|
|
|
56
109
|
/**
|
|
@@ -173,7 +226,7 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
173
226
|
|
|
174
227
|
imports.push({
|
|
175
228
|
defaultImport: "* as " + namespaceImport,
|
|
176
|
-
moduleSpecifier:
|
|
229
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
177
230
|
});
|
|
178
231
|
|
|
179
232
|
const assumedHttpMethod = getHttpMethodFromHandlerName(sf.getBaseName());
|
|
@@ -252,7 +305,7 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
252
305
|
|
|
253
306
|
imports.push({
|
|
254
307
|
defaultImport: sf.getBaseNameWithoutExtension(),
|
|
255
|
-
moduleSpecifier:
|
|
308
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
256
309
|
});
|
|
257
310
|
|
|
258
311
|
reposArr.insertElement(
|
|
@@ -288,10 +341,11 @@ autoRegisteredHandlers.push(...handlers);
|
|
|
288
341
|
const sf = this.createSourceFile(
|
|
289
342
|
["start.ts"],
|
|
290
343
|
`// Generated ${new Date()}
|
|
291
|
-
import "./generatedHandlers";
|
|
292
|
-
import "./generatedRepos";
|
|
293
|
-
import "./generatedJobs";
|
|
294
|
-
import "..${appEntryScript.replace(/\.ts/g, "")}";
|
|
344
|
+
import "./generatedHandlers${this.isEsm ? ".js" : ""}";
|
|
345
|
+
import "./generatedRepos${this.isEsm ? ".js" : ""}";
|
|
346
|
+
import "./generatedJobs${this.isEsm ? ".js" : ""}";
|
|
347
|
+
import "..${appEntryScript.replace(/\.ts/g, "")}${this.isEsm ? ".js" : ""}";
|
|
348
|
+
export default {}; // Export an empty object to make it a module
|
|
295
349
|
`
|
|
296
350
|
);
|
|
297
351
|
|
|
@@ -660,7 +714,7 @@ autoRegisteredJobs.push(...jobs);
|
|
|
660
714
|
|
|
661
715
|
imports.push({
|
|
662
716
|
defaultImport: "* as " + namespaceImport,
|
|
663
|
-
moduleSpecifier:
|
|
717
|
+
moduleSpecifier: this.getModuleSpecifier(generatedFile, sf),
|
|
664
718
|
});
|
|
665
719
|
|
|
666
720
|
// Append metadata to source file that will be part of emitted dist bundle (javascript)
|