@lenne.tech/nest-server 11.31.0 → 11.31.2

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.
Files changed (31) hide show
  1. package/CLAUDE.md +2 -0
  2. package/FRAMEWORK-API.md +1 -1
  3. package/dist/core/modules/ai/core-ai.controller.d.ts +1 -1
  4. package/dist/core/modules/ai/core-ai.controller.js +4 -5
  5. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  6. package/dist/core/modules/ai/core-ai.resolver.d.ts +1 -1
  7. package/dist/core/modules/ai/core-ai.resolver.js +7 -6
  8. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  9. package/dist/core/modules/ai/models/core-ai-conversation.model.d.ts +6 -0
  10. package/dist/core/modules/ai/models/core-ai-conversation.model.js +13 -1
  11. package/dist/core/modules/ai/models/core-ai-conversation.model.js.map +1 -1
  12. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +8 -2
  13. package/dist/core/modules/ai/services/core-ai-conversation.service.js +53 -2
  14. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  15. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +13 -1
  16. package/dist/core/modules/migrate/cli/migrate-cli.js +16 -1
  17. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  18. package/dist/core/modules/migrate/migration-runner.d.ts +4 -0
  19. package/dist/core/modules/migrate/migration-runner.js +46 -6
  20. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  21. package/dist/tsconfig.build.tsbuildinfo +1 -1
  22. package/migration-guides/11.31.0-to-11.31.1.md +89 -0
  23. package/migration-guides/11.31.1-to-11.31.2.md +127 -0
  24. package/package.json +1 -1
  25. package/src/core/modules/ai/core-ai.controller.ts +12 -9
  26. package/src/core/modules/ai/core-ai.resolver.ts +14 -9
  27. package/src/core/modules/ai/models/core-ai-conversation.model.ts +18 -1
  28. package/src/core/modules/ai/services/core-ai-conversation.service.ts +110 -4
  29. package/src/core/modules/migrate/README.md +56 -0
  30. package/src/core/modules/migrate/cli/migrate-cli.ts +27 -3
  31. package/src/core/modules/migrate/migration-runner.ts +123 -6
@@ -33,6 +33,59 @@ export interface MigrationFile {
33
33
  */
34
34
  export const DEFAULT_MIGRATION_FILE_PATTERN = /(?:(?<!\.d)\.ts|\.js)$/;
35
35
 
36
+ /**
37
+ * Matches the trailing `.ts`/`.js` extension stripped by {@link migrationId}.
38
+ * Hoisted to module level so `migrationId` allocates no per-call RegExp wrapper.
39
+ */
40
+ const MIGRATION_EXTENSION_PATTERN = /\.(ts|js)$/;
41
+
42
+ /**
43
+ * Migration identity = the timestamped file stem WITHOUT its `.ts`/`.js` extension.
44
+ *
45
+ * A migration keeps its identity when a project switches its production image from
46
+ * ts-node (`1699-foo.ts`) to compiled JavaScript (`1699-foo.js`) — the recommended
47
+ * prod setup, since the image prunes ts-node. Comparing raw filenames would make
48
+ * every compiled `.js` migration look "pending" against a state recorded under `.ts`
49
+ * names and re-run already-applied migrations (data corruption on existing DBs).
50
+ * Normalising both sides makes the transition safe with no state rewrite.
51
+ *
52
+ * @param title Migration title, usually the filename (e.g. `1699000000000-foo.ts`)
53
+ * @returns The extension-agnostic identity (e.g. `1699000000000-foo`)
54
+ * @example
55
+ * migrationId('1699000000000-foo.ts'); // '1699000000000-foo'
56
+ * migrationId('1699000000000-foo.js'); // '1699000000000-foo'
57
+ * migrationId('1699000000000-foo'); // '1699000000000-foo' (already extensionless)
58
+ */
59
+ export function migrationId(title: string): string {
60
+ return title.replace(MIGRATION_EXTENSION_PATTERN, '');
61
+ }
62
+
63
+ /**
64
+ * Parse the `NSC__MIGRATE__STRICT` environment variable into a boolean.
65
+ *
66
+ * Truthy values: `1`, `true`, `yes` (case-insensitive, surrounding whitespace ignored —
67
+ * a real risk with Docker/compose env files). Every other value means `false` (tolerate,
68
+ * the safe default); non-empty unrecognized values additionally emit a warning so a typo
69
+ * like `NSC__MIGRATE__STRICT=on` does not silently disable the control the operator
70
+ * intended to enable.
71
+ *
72
+ * Shared by the CLI (`migrate-cli.ts`) and the {@link MigrationRunner} constructor, so
73
+ * the env var behaves identically for CLI and programmatic runners.
74
+ */
75
+ export function parseStrictEnv(value: string | undefined = process.env.NSC__MIGRATE__STRICT): boolean {
76
+ if (value === undefined) {
77
+ return false;
78
+ }
79
+ const normalized = value.trim().toLowerCase();
80
+ if (['1', 'true', 'yes'].includes(normalized)) {
81
+ return true;
82
+ }
83
+ if (normalized !== '' && !['0', 'false', 'no'].includes(normalized)) {
84
+ console.warn(`[migrate] Unrecognized NSC__MIGRATE__STRICT value "${value}" — treating as false (tolerate).`);
85
+ }
86
+ return false;
87
+ }
88
+
36
89
  /**
37
90
  * Migration runner configuration
38
91
  */
@@ -43,6 +96,23 @@ export interface MigrationRunnerOptions {
43
96
  pattern?: RegExp;
44
97
  /** State store for tracking migrations */
45
98
  stateStore: MongoStateStore;
99
+ /**
100
+ * Fail hard when a migration recorded in the state has no file on disk.
101
+ *
102
+ * Default: resolved from the `NSC__MIGRATE__STRICT` environment variable (see
103
+ * {@link parseStrictEnv}), falling back to `false` (tolerate) — recorded migrations
104
+ * whose files were deleted are ignored: `up()` skips them (with a warning) and the
105
+ * server still starts. Migrations are tracked in git and can be restored, so old
106
+ * migration files can be pruned without blocking boot. Set `true` to enforce
107
+ * state/disk integrity (missing file → error in `up()` and `migrate list`).
108
+ *
109
+ * Applies to `up()` and `status()`/`migrate list` only — `down()` ALWAYS fails hard
110
+ * on a missing rollback file, because rollback is an explicit operator action and
111
+ * never a boot path (an exit 0 with no rollback performed would mislead scripts).
112
+ *
113
+ * @see `--strict` CLI flag and `NSC__MIGRATE__STRICT` env var in `cli/migrate-cli.ts`
114
+ */
115
+ strict?: boolean;
46
116
  }
47
117
 
48
118
  /**
@@ -72,12 +142,21 @@ export class MigrationRunner {
72
142
  private pattern: RegExp;
73
143
 
74
144
  constructor(options: MigrationRunnerOptions) {
75
- this.options = options;
145
+ // Resolve the strict default from the environment so NSC__MIGRATE__STRICT works
146
+ // identically for programmatic runners and the CLI (which parses it itself).
147
+ this.options = { ...options, strict: options.strict ?? parseStrictEnv() };
76
148
  this.pattern = options.pattern || DEFAULT_MIGRATION_FILE_PATTERN;
77
149
  }
78
150
 
79
151
  /**
80
152
  * Load all migration files from the migrations directory
153
+ *
154
+ * Files are deduplicated by {@link migrationId}: when both `foo.ts` and `foo.js`
155
+ * are present (overlapping `outDir`, source + build output copied into one image),
156
+ * they are ONE migration — loading both would execute it twice in a single `up()`
157
+ * run (the exact double-execution the identity concept exists to prevent). The
158
+ * `.js` file wins deterministically (`.js` sorts before `.ts`), matching the
159
+ * compiled-production intent; the duplicate is skipped with a warning.
81
160
  */
82
161
  private async loadMigrationFiles(): Promise<MigrationFile[]> {
83
162
  const files = fs
@@ -86,8 +165,17 @@ export class MigrationRunner {
86
165
  .sort(); // Sort alphabetically (timestamp-based filenames will be in order)
87
166
 
88
167
  const migrations: MigrationFile[] = [];
168
+ const seenIds = new Set<string>();
89
169
 
90
170
  for (const file of files) {
171
+ const id = migrationId(file);
172
+ if (seenIds.has(id)) {
173
+ console.warn(
174
+ `[migrate] duplicate files for migration "${id}" — skipping ${file} (a same-named file was already loaded)`,
175
+ );
176
+ continue;
177
+ }
178
+
91
179
  const filePath = path.join(this.options.migrationsDirectory, file);
92
180
 
93
181
  const module = require(filePath);
@@ -101,6 +189,7 @@ export class MigrationRunner {
101
189
  const timestampMatch = file.match(/^(\d+)-/);
102
190
  const timestamp = timestampMatch ? parseInt(timestampMatch[1], 10) : Date.now();
103
191
 
192
+ seenIds.add(id);
104
193
  migrations.push({
105
194
  down: module.down,
106
195
  filePath,
@@ -121,9 +210,24 @@ export class MigrationRunner {
121
210
 
122
211
  const allMigrations = await this.loadMigrationFiles();
123
212
  const state = await this.options.stateStore.loadAsync();
124
- const completedMigrations = (state.migrations || []).map((m) => m.title);
213
+ const recorded = state.migrations || [];
214
+ const presentIds = new Set(allMigrations.map((m) => migrationId(m.title)));
215
+
216
+ // Recorded migrations whose file is gone. Tolerated by default (git-tracked and
217
+ // restorable); in strict mode this is a hard error so integrity drift is caught.
218
+ const missing = recorded.filter((m) => !presentIds.has(migrationId(m.title)));
219
+ if (missing.length > 0) {
220
+ const list = missing.map((m) => m.title).join(', ');
221
+ if (this.options.strict) {
222
+ throw new Error(`Strict mode: ${missing.length} recorded migration file(s) missing: ${list}`);
223
+ }
224
+ console.warn(
225
+ `[migrate] ${missing.length} recorded migration file(s) missing — tolerated (strict=false): ${list}`,
226
+ );
227
+ }
125
228
 
126
- const pendingMigrations = allMigrations.filter((m) => !completedMigrations.includes(m.title));
229
+ const completedIds = new Set(recorded.map((m) => migrationId(m.title)));
230
+ const pendingMigrations = allMigrations.filter((m) => !completedIds.has(migrationId(m.title)));
127
231
 
128
232
  if (pendingMigrations.length === 0) {
129
233
  console.log('No pending migrations');
@@ -181,10 +285,14 @@ export class MigrationRunner {
181
285
 
182
286
  const lastMigration = completedMigrations[completedMigrations.length - 1];
183
287
  const allMigrations = await this.loadMigrationFiles();
184
- const migrationToRollback = allMigrations.find((m) => m.title === lastMigration.title);
288
+ const rollbackId = migrationId(lastMigration.title);
289
+ const migrationToRollback = allMigrations.find((m) => migrationId(m.title) === rollbackId);
185
290
 
186
291
  if (!migrationToRollback) {
187
- throw new Error(`Migration file not found: ${lastMigration.title}`);
292
+ // Always a hard error, independent of `strict`: down() is an explicit operator
293
+ // action and never a boot path — the boot-tolerance rationale does not apply,
294
+ // and an exit 0 with no rollback performed would mislead scripted rollbacks.
295
+ throw new Error(`Migration file not found: ${lastMigration.title} — restore the file from git to roll back.`);
188
296
  }
189
297
 
190
298
  if (!migrationToRollback.down) {
@@ -216,18 +324,27 @@ export class MigrationRunner {
216
324
 
217
325
  /**
218
326
  * Get migration status
327
+ *
328
+ * `missing` lists recorded migrations whose file is gone from disk (identity-based,
329
+ * so a `.ts`-recorded migration with a compiled `.js` on disk is NOT missing) — the
330
+ * same integrity drift `up()` warns about, surfaced here so `migrate list` can show
331
+ * it before a deploy or file pruning.
219
332
  */
220
333
  async status(): Promise<{
221
334
  completed: string[];
335
+ missing: string[];
222
336
  pending: string[];
223
337
  }> {
224
338
  const allMigrations = await this.loadMigrationFiles();
225
339
  const state = await this.options.stateStore.loadAsync();
226
340
  const completedMigrations = (state.migrations || []).map((m) => m.title);
341
+ const completedIds = new Set(completedMigrations.map(migrationId));
342
+ const presentIds = new Set(allMigrations.map((m) => migrationId(m.title)));
227
343
 
228
344
  return {
229
345
  completed: completedMigrations,
230
- pending: allMigrations.filter((m) => !completedMigrations.includes(m.title)).map((m) => m.title),
346
+ missing: completedMigrations.filter((title) => !presentIds.has(migrationId(title))),
347
+ pending: allMigrations.filter((m) => !completedIds.has(migrationId(m.title))).map((m) => m.title),
231
348
  };
232
349
  }
233
350