@smartive/graphql-magic 22.2.0 → 22.3.0-next.1

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.
@@ -160,35 +160,27 @@ export class MigrationGenerator {
160
160
  );
161
161
 
162
162
  // Update fields
163
- const existingFields = model.fields.filter((field) => {
163
+ const rawExistingFields = model.fields.filter((field) => {
164
+ if (!field.generateAs) {
165
+ return false;
166
+ }
167
+
164
168
  const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
165
169
  if (!col) {
166
170
  return false;
167
171
  }
168
172
 
169
- if ((!field.nonNull && !col.is_nullable) || (field.nonNull && col.is_nullable)) {
173
+ if (col.generation_expression !== field.generateAs) {
170
174
  return true;
171
175
  }
172
176
 
173
- if (!field.kind || field.kind === 'primitive') {
174
- if (field.type === 'Int') {
175
- if (col.data_type !== 'integer') {
176
- return true;
177
- }
178
- }
179
- if (field.type === 'Float') {
180
- if (field.double) {
181
- if (col.data_type !== 'double precision') {
182
- return true;
183
- }
184
- } else if (col.data_type !== 'numeric') {
185
- return true;
186
- }
187
- }
188
- }
189
-
190
- return false;
177
+ return this.hasChanged(model, field);
191
178
  });
179
+ if (rawExistingFields.length) {
180
+ this.updateFieldsRaw(model, rawExistingFields, up, down);
181
+ }
182
+
183
+ const existingFields = model.fields.filter((field) => !field.generateAs && this.hasChanged(model, field));
192
184
  this.updateFields(model, existingFields, up, down);
193
185
  }
194
186
 
@@ -375,6 +367,10 @@ export class MigrationGenerator {
375
367
  for (const field of fields) {
376
368
  alter.push(() => this.column(field, { setNonNull: field.defaultValue !== undefined }));
377
369
 
370
+ if (field.generateAs) {
371
+ continue;
372
+ }
373
+
378
374
  // If the field is not nullable but has no default, write placeholder code
379
375
  if (field.nonNull && field.defaultValue === undefined) {
380
376
  updates.push(() => this.writer.write(`${field.name}: 'TODO',`).newLine());
@@ -405,13 +401,63 @@ export class MigrationGenerator {
405
401
 
406
402
  down.push(() => {
407
403
  this.alterTable(model.name, () => {
408
- for (const { kind, name } of fields) {
404
+ for (const { kind, name } of fields.toReversed()) {
409
405
  this.dropColumn(kind === 'relation' ? `${name}Id` : name);
410
406
  }
411
407
  });
412
408
  });
413
409
  }
414
410
 
411
+ private updateFieldsRaw(model: EntityModel, fields: EntityField[], up: Callbacks, down: Callbacks) {
412
+ if (!fields.length) {
413
+ return;
414
+ }
415
+
416
+ up.push(() => {
417
+ this.alterTableRaw(model.name, () => {
418
+ for (const [index, field] of fields.entries()) {
419
+ this.columnRaw(field, { alter: true }, index);
420
+ }
421
+ });
422
+ });
423
+
424
+ down.push(() => {
425
+ this.alterTableRaw(model.name, () => {
426
+ for (const [index, field] of fields.entries()) {
427
+ this.columnRaw(field, { alter: true }, index);
428
+ }
429
+ });
430
+ });
431
+
432
+ if (isUpdatableModel(model)) {
433
+ const updatableFields = fields.filter(isUpdatableField);
434
+ if (!updatableFields.length) {
435
+ return;
436
+ }
437
+
438
+ up.push(() => {
439
+ this.alterTable(`${model.name}Revision`, () => {
440
+ for (const [index, field] of updatableFields.entries()) {
441
+ this.columnRaw(field, { alter: true }, index);
442
+ }
443
+ });
444
+ });
445
+
446
+ down.push(() => {
447
+ this.alterTable(`${model.name}Revision`, () => {
448
+ for (const [index, field] of updatableFields.entries()) {
449
+ this.columnRaw(
450
+ field,
451
+ { alter: true },
452
+ index,
453
+ summonByName(this.columns[model.name], field.kind === 'relation' ? `${field.name}Id` : field.name),
454
+ );
455
+ }
456
+ });
457
+ });
458
+ }
459
+ }
460
+
415
461
  private updateFields(model: EntityModel, fields: EntityField[], up: Callbacks, down: Callbacks) {
416
462
  if (!fields.length) {
417
463
  return;
@@ -572,6 +618,12 @@ export class MigrationGenerator {
572
618
  .blankLine();
573
619
  }
574
620
 
621
+ private alterTableRaw(table: string, block: () => void) {
622
+ this.writer.write(`await knex.raw('ALTER TABLE "${table}"`);
623
+ block();
624
+ this.writer.write(`');`).newLine().blankLine();
625
+ }
626
+
575
627
  private alterTable(table: string, block: () => void) {
576
628
  return this.writer
577
629
  .write(`await knex.schema.alterTable('${table}', (table) => `)
@@ -605,29 +657,125 @@ export class MigrationGenerator {
605
657
  return value;
606
658
  }
607
659
 
660
+ private columnRaw(
661
+ { name, ...field }: EntityField,
662
+ { setNonNull = true, alter = false } = {},
663
+ index: number,
664
+ toColumn?: Column,
665
+ ) {
666
+ const nonNull = () => {
667
+ if (setNonNull) {
668
+ if (toColumn) {
669
+ if (toColumn.is_nullable) {
670
+ return false;
671
+ }
672
+
673
+ return true;
674
+ }
675
+ if (field.nonNull) {
676
+ return true;
677
+ }
678
+
679
+ return false;
680
+ }
681
+ };
682
+ const kind = field.kind;
683
+ if (field.generateAs) {
684
+ let type = '';
685
+ switch (kind) {
686
+ case undefined:
687
+ case 'primitive':
688
+ switch (field.type) {
689
+ case 'Float':
690
+ type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
691
+ break;
692
+ default:
693
+ throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
694
+ }
695
+ break;
696
+ default:
697
+ throw new Error(`Generated columns of kind ${kind} are not supported yet.`);
698
+ }
699
+ if (index) {
700
+ this.writer.write(`,`);
701
+ }
702
+ if (alter) {
703
+ this.writer.write(` ALTER COLUMN "${name}" TYPE ${type}`);
704
+ if (setNonNull) {
705
+ if (nonNull()) {
706
+ this.writer.write(`, ALTER COLUMN "${name}" SET NOT NULL`);
707
+ } else {
708
+ this.writer.write(`, ALTER COLUMN "${name}" DROP NOT NULL`);
709
+ }
710
+ }
711
+ this.writer.write(`, ALTER COLUMN "${name}" SET EXPRESSION AS (${field.generateAs})`);
712
+ } else {
713
+ this.writer.write(
714
+ `${alter ? 'ALTER' : 'ADD'} COLUMN "${name}" ${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED`,
715
+ );
716
+ }
717
+
718
+ return;
719
+ }
720
+
721
+ throw new Error(`Only generated columns can be created with columnRaw`);
722
+ }
723
+
608
724
  private column(
609
725
  { name, primary, list, ...field }: EntityField,
610
726
  { setUnique = true, setNonNull = true, alter = false, foreign = true, setDefault = true } = {},
611
727
  toColumn?: Column,
612
728
  ) {
613
- const col = (what?: string) => {
614
- if (what) {
615
- this.writer.write(what);
616
- }
729
+ const nonNull = () => {
617
730
  if (setNonNull) {
618
731
  if (toColumn) {
619
732
  if (toColumn.is_nullable) {
620
- this.writer.write(`.nullable()`);
621
- } else {
622
- this.writer.write('.notNullable()');
623
- }
624
- } else {
625
- if (field.nonNull) {
626
- this.writer.write(`.notNullable()`);
627
- } else {
628
- this.writer.write('.nullable()');
733
+ return false;
629
734
  }
735
+
736
+ return true;
630
737
  }
738
+ if (field.nonNull) {
739
+ return true;
740
+ }
741
+
742
+ return false;
743
+ }
744
+ };
745
+ const kind = field.kind;
746
+ if (field.generateAs) {
747
+ let type = '';
748
+ switch (kind) {
749
+ case undefined:
750
+ case 'primitive':
751
+ switch (field.type) {
752
+ case 'Float':
753
+ type = `decimal(${field.precision ?? 'undefined'}, ${field.scale ?? 'undefined'})`;
754
+ break;
755
+ default:
756
+ throw new Error(`Generated columns of kind ${kind} and type ${field.type} are not supported yet.`);
757
+ }
758
+ break;
759
+ default:
760
+ throw new Error(`Generated columns of kind ${kind} are not supported yet.`);
761
+ }
762
+ this.writer.write(
763
+ `table.specificType('${name}', '${type}${nonNull() ? ' not null' : ''} GENERATED ALWAYS AS (${field.generateAs}) STORED')`,
764
+ );
765
+ if (alter) {
766
+ this.writer.write('.alter()');
767
+ }
768
+ this.writer.write(';').newLine();
769
+
770
+ return;
771
+ }
772
+
773
+ const col = (what?: string) => {
774
+ if (what) {
775
+ this.writer.write(what);
776
+ }
777
+ if (setNonNull) {
778
+ this.writer.write(nonNull() ? '.notNullable()' : '.nullable()');
631
779
  }
632
780
  if (setDefault && field.defaultValue !== undefined) {
633
781
  this.writer.write(`.defaultTo(${this.value(field.defaultValue)})`);
@@ -642,7 +790,6 @@ export class MigrationGenerator {
642
790
  }
643
791
  this.writer.write(';').newLine();
644
792
  };
645
- const kind = field.kind;
646
793
  switch (kind) {
647
794
  case undefined:
648
795
  case 'primitive':
@@ -716,6 +863,44 @@ export class MigrationGenerator {
716
863
  private getColumn(tableName: string, columnName: string) {
717
864
  return this.columns[tableName].find((col) => col.name === columnName);
718
865
  }
866
+
867
+ private hasChanged(model: EntityModel, field: EntityField) {
868
+ const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
869
+ if (!col) {
870
+ return false;
871
+ }
872
+
873
+ if (field.generateAs) {
874
+ if (col.generation_expression !== field.generateAs) {
875
+ throw new Error(
876
+ `Column ${col.name} has specific type ${col.generation_expression} but expected ${field.generateAs}`,
877
+ );
878
+ }
879
+ }
880
+
881
+ if ((!field.nonNull && !col.is_nullable) || (field.nonNull && col.is_nullable)) {
882
+ return true;
883
+ }
884
+
885
+ if (!field.kind || field.kind === 'primitive') {
886
+ if (field.type === 'Int') {
887
+ if (col.data_type !== 'integer') {
888
+ return true;
889
+ }
890
+ }
891
+ if (field.type === 'Float') {
892
+ if (field.double) {
893
+ if (col.data_type !== 'double precision') {
894
+ return true;
895
+ }
896
+ } else if (col.data_type !== 'numeric') {
897
+ return true;
898
+ }
899
+ }
900
+ }
901
+
902
+ return false;
903
+ }
719
904
  }
720
905
 
721
906
  export const getMigrationDate = () => {
@@ -90,6 +90,7 @@ export type EntityFieldDefinition = FieldDefinitionBase &
90
90
  indent?: boolean;
91
91
  // If true the field is hidden in the admin interface
92
92
  hidden?: boolean;
93
+ generateAs?: string;
93
94
 
94
95
  // Temporary fields for the generation of migrations
95
96
  deleted?: true;