@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.
@@ -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
- private auth?;
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
  *
@@ -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, autoRegisteredHandlers_1, _a, handler, assumedHttpMethod, pathParams, _b, _c, param;
416
- var _d, _e, _f;
417
- return __generator(this, function (_g) {
418
- for (_i = 0, autoRegisteredHandlers_1 = exports.autoRegisteredHandlers; _i < autoRegisteredHandlers_1.length; _i++) {
419
- _a = autoRegisteredHandlers_1[_i], handler = _a.handler, assumedHttpMethod = _a.assumedHttpMethod;
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 (!!((_d = handler.__params) === null || _d === void 0 ? void 0 : _d.length)) {
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 (_b = 0, _c = handler.__params; _b < _c.length; _b++) {
431
- param = _c[_b];
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: (_e = handler.__schemas) === null || _e === void 0 ? void 0 : _e.reqSchema,
445
- resSchema: (_f = handler.__schemas) === null || _f === void 0 ? void 0 : _f.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
- type Query = Request["query"];
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
@@ -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
- private collectionName;
5
- private db;
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: Partial<Model>): Promise<Model | null>;
18
- updateMany<U = Partial<Model>>(query: any, model: U): Promise<number>;
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
- private buildId;
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 {};
@@ -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
- return [4 /*yield*/, this.collection.updateOne({ _id: oid }, { $set: model })];
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 (_a) {
155
- switch (_a.label) {
156
- case 0: return [4 /*yield*/, this.collection.updateMany(query, {
157
- $set: model,
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 = (_a.sent()).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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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.2",
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
- "prepublish": "npm run build",
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
- "gitHead": "b5be77c75e10067c81e0ecab8716b30f99833b83"
69
+ "peerDependencies": {
70
+ "mongodb": ">=3.7.0 <7.0.0"
71
+ },
72
+ "gitHead": "133c6f50b3119f8264f6350e2043c171781b583f"
70
73
  }
@@ -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
- private auth?: FlinkAuthPlugin;
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,
@@ -5,24 +5,25 @@ import { FlinkError } from "./FlinkErrors";
5
5
  import { FlinkResponse } from "./FlinkResponse";
6
6
 
7
7
  export enum HttpMethod {
8
- get = "get",
9
- post = "post",
10
- put = "put",
11
- delete = "delete",
8
+ get = "get",
9
+ post = "post",
10
+ put = "put",
11
+ delete = "delete",
12
12
  }
13
13
 
14
14
  type Params = Request["params"];
15
- type Query = Request["query"];
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
- * HTTP method which this handlers responds to.
36
- *
37
- * Will if not set attempt to extract HTTP method based
38
- * on handler file name prefix, for example `GetFoo.ts` will assume
39
- * HTTP method `GET`.
40
- */
41
- method?: HttpMethod;
42
-
43
- /**
44
- * Route path including any path params.
45
- * Example: `/user/:id`
46
- */
47
- path: string;
48
-
49
- /**
50
- * Generates mock response based on handlers response schema.
51
- *
52
- * Will be ignored if handler does not have any response schema defined.
53
- *
54
- * This should only be used during development 💥
55
- */
56
- mockApi?: boolean;
57
-
58
- /**
59
- * Set permissions needed to access route if route requires authentication.
60
- */
61
- permissions?: string | string[];
62
-
63
- /**
64
- * Optional documentation of endpoint. Can be used for example in API docs.
65
- * Supports markdown strings.
66
- */
67
- docs?: string; // TODO
68
-
69
- /**
70
- * If handler should not be auto registered
71
- */
72
- skipAutoRegister?: boolean;
73
-
74
- /**
75
- * I.e. filename or plugin name that describes where handler origins from
76
- */
77
- origin?: string;
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
- Ctx extends FlinkContext,
86
- ReqSchema = any,
87
- ResSchema = any,
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
- default: Handler<any, any, any, any, any>;
117
- Route?: RouteProps;
118
- /**
119
- * Name of schemas, is set at compile time by Flink compiler.
120
- */
121
- __schemas?: {
122
- reqSchema?: JSONSchema;
123
- resSchema?: JSONSchema;
124
- };
125
- /**
126
- * Typescript source file name, is set at compile time by Flink compiler.
127
- */
128
- __file?: string;
129
-
130
- /**
131
- * Description of query params, is set at compile time by Flink compiler.
132
- */
133
- __query?: QueryParamMetadata[];
134
-
135
- /**
136
- * Description of path params, is set at compile time by Flink compiler.
137
- */
138
- __params?: QueryParamMetadata[];
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
- name: string;
143
- description: string;
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(private collectionName: string, private db: Db) {
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: Partial<Model>): Promise<Model | null> {
54
+ async updateOne(id: string | ObjectId, model: PartialModel<Model>): Promise<Model | null> {
49
55
  const oid = this.buildId(id);
50
56
 
51
- await this.collection.updateOne({ _id: oid }, { $set: model });
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 = Partial<Model>>(query: any, model: U): Promise<number> {
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: model as any,
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
- private buildId(id: string | ObjectId) {
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, Config, createFormatter, createParser, Schema, SchemaGenerator } from "ts-json-schema-generator";
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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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: generatedFile.getRelativePathAsModuleSpecifierTo(sf),
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)