uql-codemod 0.39.2 → 0.40.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.
Files changed (2) hide show
  1. package/dist/transform.js +64 -1
  2. package/package.json +1 -1
package/dist/transform.js CHANGED
@@ -285,7 +285,8 @@ function foreignKeyColumn(property, column, relation, checker) {
285
285
  }
286
286
  const indent = ' '.repeat(property.getSourceFile().getLineAndCharacterOfPosition(property.getStart()).character);
287
287
  const type = keyTypeSource(key, property, checker);
288
- return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type};\n${indent}`);
288
+ // `| null`, as on every column the codemod touches: a foreign key holds one like any other.
289
+ return inserted(property, `@Field({ references: () => ${target} }) ${column}?: ${type} | null;\n${indent}`);
289
290
  }
290
291
  /** The key's type as written where that is in scope, the same file, or as the checker spells it out. */
291
292
  function keyTypeSource(key, at, checker) {
@@ -377,6 +378,7 @@ function rewriteEntityOptions(options, owner, ctx) {
377
378
  }
378
379
  for (const field of objectProperties(fields)) {
379
380
  renameSqlOption(optionsOf(field.initializer), 'virtual', 'computed', field, owner, ctx);
381
+ admitNullOnDefined(field, owner, ctx);
380
382
  }
381
383
  for (const filter of objectProperties(propertyValue(options, 'filters'))) {
382
384
  renameOption(optionsOf(filter.initializer), 'condition', 'where', filter, ctx);
@@ -669,6 +671,9 @@ function rewriteProperty(node, ctx) {
669
671
  }
670
672
  const options = decoratorOptions(decorator);
671
673
  if (FIELD_DECORATORS.has(name)) {
674
+ if (name === 'Field') {
675
+ admitNull(options, node, ctx);
676
+ }
672
677
  addFieldType(decorator, node, ctx);
673
678
  // Options it cannot read may still hold `virtual`, so they are reported where they mention it.
674
679
  if (options.kind === 'opaque' && /\bvirtual\b/.test(decorator.getText())) {
@@ -692,6 +697,64 @@ function rewriteProperty(node, ctx) {
692
697
  }
693
698
  unwrapRelationAlias(node, ctx);
694
699
  }
700
+ /**
701
+ * A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so the property
702
+ * says so too. A key is NOT NULL on every engine, and a relation aggregate is typed by the aggregate,
703
+ * which declares its own `null` or none, so neither is touched.
704
+ */
705
+ function admitNull(options, node, ctx) {
706
+ // Options it cannot read may state `nullable` themselves, and appending to the property would then
707
+ // contradict the column. `addFieldType` already reports the same options, so this adds no second note.
708
+ if (options.kind === 'opaque') {
709
+ return;
710
+ }
711
+ if (declaresNotNull(options) ||
712
+ findProperty(options, 'isId') ||
713
+ (findProperty(options, 'computed') && !findProperty(options, 'type'))) {
714
+ return;
715
+ }
716
+ if (!node.type) {
717
+ ctx.notes.push(`${ctx.describe(node)}: declare '${propertyKey(node.name)}' as a type that admits 'null', which the column holds`);
718
+ return;
719
+ }
720
+ if (admitsNull(node.type)) {
721
+ return;
722
+ }
723
+ ctx.edits.push(appended(node.type, ' | null'));
724
+ }
725
+ /**
726
+ * The same null rule where `defineEntity` names the field: the property is on the class the call takes,
727
+ * so the declaration is found there rather than under a decorator.
728
+ */
729
+ function admitNullOnDefined(field, owner, ctx) {
730
+ if (!ts.isPropertyAssignment(field)) {
731
+ return;
732
+ }
733
+ const key = propertyKey(field.name);
734
+ const declaration = key
735
+ ? instanceTypeOf(owner.entity, ctx.checker)
736
+ .getProperty(key)
737
+ ?.declarations?.find((it) => ts.isPropertyDeclaration(it))
738
+ : undefined;
739
+ if (declaration?.type) {
740
+ admitNull(optionsOf(field.initializer), declaration, ctx);
741
+ }
742
+ }
743
+ /** Whether the options say the column refuses `null`; `nullable: true` is the default said out loud. */
744
+ function declaresNotNull(options) {
745
+ const nullable = findProperty(options, 'nullable');
746
+ return !!nullable && ts.isPropertyAssignment(nullable) && nullable.initializer.kind === ts.SyntaxKind.FalseKeyword;
747
+ }
748
+ /**
749
+ * Whether a written type already admits `null`, wherever it sits in the union. In a type position `null`
750
+ * parses as a literal type rather than a keyword, which is why the kind is read off `literal`.
751
+ */
752
+ function admitsNull(type) {
753
+ if (ts.isLiteralTypeNode(type)) {
754
+ return type.literal.kind === ts.SyntaxKind.NullKeyword;
755
+ }
756
+ return ts.isUnionTypeNode(type) && type.types.some(admitsNull);
757
+ }
695
758
  /** Names a decorator that no longer exists, wherever it appears. */
696
759
  function reportRemovedDecorators(node, ctx) {
697
760
  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.40.0",
6
6
  "type": "module",
7
7
  "engines": {
8
8
  "node": ">=24"