uql-codemod 0.39.2 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
File without changes
package/dist/transform.js CHANGED
@@ -23,7 +23,10 @@ const REMOVED_DECORATORS = new Map([
23
23
  * same reason as the decorators: what replaces each is decided at its call site.
24
24
  */
25
25
  const REMOVED_EXPORTS = new Map([
26
- ['setQuerierPool', 'pass the pool where it is used: `createFetchHandler({ pool })`, `querierMiddleware({ pool })`'],
26
+ [
27
+ 'setQuerierPool',
28
+ 'pass the pool where it is used: `createFetchHandler({ pool, include })`, `querierMiddleware({ pool, include })`',
29
+ ],
27
30
  ['getQuerierPool', 'take the pool from the module that builds it, or from Nest DI'],
28
31
  ['getQuerier', 'use `pool.withQuerier(...)` / `pool.transaction(...)`, which release the connection'],
29
32
  ['QueryWhereFieldMap', 'use `QueryWhere`'],
@@ -285,7 +288,8 @@ function foreignKeyColumn(property, column, relation, checker) {
285
288
  }
286
289
  const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
287
290
  const type = keyTypeSource(key, property, checker);
288
- return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type};\n${indent}`);
291
+ // `| null`, as on every column the codemod touches: a foreign key holds one like any other.
292
+ return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indent}`);
289
293
  }
290
294
  /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
291
295
  function keyTypeSource(key, at, checker) {
@@ -377,6 +381,7 @@ function rewriteEntityOptions(options, owner, ctx) {
377
381
  }
378
382
  for (const field of objectProperties(fields)) {
379
383
  renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
384
+ admitNullOnDefined(field, owner, ctx);
380
385
  }
381
386
  for (const filter of objectProperties(propertyValue(options, 'filters'))) {
382
387
  renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
@@ -669,6 +674,9 @@ function rewriteProperty(node, ctx) {
669
674
  }
670
675
  const options = decoratorOptions(decorator);
671
676
  if (FIELD_DECORATORS.has(name)) {
677
+ if (name === 'Field') {
678
+ admitNull(options, node, ctx);
679
+ }
672
680
  addFieldType(decorator, node, ctx);
673
681
  // Options it cannot read may still hold `virtual`, so they are reported where they mention it.
674
682
  if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
@@ -692,6 +700,64 @@ function rewriteProperty(node, ctx) {
692
700
  }
693
701
  unwrapRelationAlias(node, ctx);
694
702
  }
703
+ /**
704
+ * A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so the property
705
+ * says so too. A key is NOT NULL on every engine, and a relation aggregate is typed by the aggregate,
706
+ * which declares its own `null` or none, so neither is touched.
707
+ */
708
+ function admitNull(options, node, ctx) {
709
+ // Options it cannot read may state `nullable` themselves, and appending to the property would then
710
+ // contradict the column. `addFieldType` already reports the same options, so this adds no second note.
711
+ if (options.kind === 'opaque') {
712
+ return;
713
+ }
714
+ if (declaresNotNull(options) ||
715
+ findProperty(options, 'isId') ||
716
+ (findProperty(options, 'computed') && !findProperty(options, 'type'))) {
717
+ return;
718
+ }
719
+ if (!node.type) {
720
+ ctx.notes.push(`${ctx.describe(node)}: declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
721
+ return;
722
+ }
723
+ if (admitsNull(node.type)) {
724
+ return;
725
+ }
726
+ ctx.edits.push(appended(node.type, ' | null'));
727
+ }
728
+ /**
729
+ * The same null rule where `defineEntity` names the field: the property is on the class the call takes,
730
+ * so the declaration is found there rather than under a decorator.
731
+ */
732
+ function admitNullOnDefined(field, owner, ctx) {
733
+ if (!ts.isPropertyAssignment(field)) {
734
+ return;
735
+ }
736
+ const key = propertyKey(field.name);
737
+ const declaration = key
738
+ ? instanceTypeOf(owner.entity, ctx.checker)
739
+ .getProperty(key)
740
+ ?.declarations?.find((it) => ts.isPropertyDeclaration(it))
741
+ : undefined;
742
+ if (declaration?.type) {
743
+ admitNull(optionsOf(field.initializer), declaration, ctx);
744
+ }
745
+ }
746
+ /** Whether the options say the column refuses `null`; `nullable: true` is the default said out loud. */
747
+ function declaresNotNull(options) {
748
+ const nullable = findProperty(options, 'nullable');
749
+ return !!nullable && ts.isPropertyAssignment(nullable) && nullable.initializer.kind === ts.SyntaxKind.FalseKeyword;
750
+ }
751
+ /**
752
+ * Whether a written type already admits `null`, wherever it sits in the union. In a type position `null`
753
+ * parses as a literal type rather than a keyword, which is why the kind is read off `literal`.
754
+ */
755
+ function admitsNull(type) {
756
+ if (ts.isLiteralTypeNode(type)) {
757
+ return type.literal.kind === ts.SyntaxKind.NullKeyword;
758
+ }
759
+ return ts.isUnionTypeNode(type) && type.types.some(admitsNull);
760
+ }
695
761
  /** Names a decorator that no longer exists, wherever it appears. */
696
762
  function reportRemovedDecorators(node, ctx) {
697
763
  for (const decorator of decoratorsOf(node)) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "uql-codemod",
3
3
  "description": "Upgrade uql-orm across versions that introduce breaking changes",
4
4
  "license": "MIT",
5
- "version": "0.39.2",
5
+ "version": "0.41.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"