@smartive/graphql-magic 22.1.0 → 22.2.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.
@@ -31,6 +31,7 @@ export class MigrationGenerator {
31
31
  private columns: Record<string, Column[]> = {};
32
32
  private uuidUsed?: boolean;
33
33
  private nowUsed?: boolean;
34
+ public needsMigration = false;
34
35
 
35
36
  constructor(
36
37
  knex: Knex,
@@ -159,35 +160,27 @@ export class MigrationGenerator {
159
160
  );
160
161
 
161
162
  // Update fields
162
- const existingFields = model.fields.filter((field) => {
163
+ const rawExistingFields = model.fields.filter((field) => {
164
+ if (!field.generateAs) {
165
+ return false;
166
+ }
167
+
163
168
  const col = this.getColumn(model.name, field.kind === 'relation' ? `${field.name}Id` : field.name);
164
169
  if (!col) {
165
170
  return false;
166
171
  }
167
172
 
168
- if ((!field.nonNull && !col.is_nullable) || (field.nonNull && col.is_nullable)) {
173
+ if (col.generation_expression !== field.generateAs) {
169
174
  return true;
170
175
  }
171
176
 
172
- if (!field.kind || field.kind === 'primitive') {
173
- if (field.type === 'Int') {
174
- if (col.data_type !== 'integer') {
175
- return true;
176
- }
177
- }
178
- if (field.type === 'Float') {
179
- if (field.double) {
180
- if (col.data_type !== 'double precision') {
181
- return true;
182
- }
183
- } else if (col.data_type !== 'numeric') {
184
- return true;
185
- }
186
- }
187
- }
188
-
189
- return false;
177
+ return this.hasChanged(model, field);
190
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));
191
184
  this.updateFields(model, existingFields, up, down);
192
185
  }
193
186
 
@@ -320,6 +313,9 @@ export class MigrationGenerator {
320
313
  writer.writeLine(`const now = date();`).blankLine();
321
314
  }
322
315
 
316
+ if (up.length || down.length) {
317
+ this.needsMigration = true;
318
+ }
323
319
  this.migration('up', up);
324
320
  this.migration('down', down.reverse());
325
321
 
@@ -371,6 +367,10 @@ export class MigrationGenerator {
371
367
  for (const field of fields) {
372
368
  alter.push(() => this.column(field, { setNonNull: field.defaultValue !== undefined }));
373
369
 
370
+ if (field.generateAs) {
371
+ continue;
372
+ }
373
+
374
374
  // If the field is not nullable but has no default, write placeholder code
375
375
  if (field.nonNull && field.defaultValue === undefined) {
376
376
  updates.push(() => this.writer.write(`${field.name}: 'TODO',`).newLine());
@@ -401,13 +401,63 @@ export class MigrationGenerator {
401
401
 
402
402
  down.push(() => {
403
403
  this.alterTable(model.name, () => {
404
- for (const { kind, name } of fields) {
404
+ for (const { kind, name } of fields.toReversed()) {
405
405
  this.dropColumn(kind === 'relation' ? `${name}Id` : name);
406
406
  }
407
407
  });
408
408
  });
409
409
  }
410
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
+
411
461
  private updateFields(model: EntityModel, fields: EntityField[], up: Callbacks, down: Callbacks) {
412
462
  if (!fields.length) {
413
463
  return;
@@ -568,6 +618,12 @@ export class MigrationGenerator {
568
618
  .blankLine();
569
619
  }
570
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
+
571
627
  private alterTable(table: string, block: () => void) {
572
628
  return this.writer
573
629
  .write(`await knex.schema.alterTable('${table}', (table) => `)
@@ -601,29 +657,125 @@ export class MigrationGenerator {
601
657
  return value;
602
658
  }
603
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
+
604
724
  private column(
605
725
  { name, primary, list, ...field }: EntityField,
606
726
  { setUnique = true, setNonNull = true, alter = false, foreign = true, setDefault = true } = {},
607
727
  toColumn?: Column,
608
728
  ) {
609
- const col = (what?: string) => {
610
- if (what) {
611
- this.writer.write(what);
612
- }
729
+ const nonNull = () => {
613
730
  if (setNonNull) {
614
731
  if (toColumn) {
615
732
  if (toColumn.is_nullable) {
616
- this.writer.write(`.nullable()`);
617
- } else {
618
- this.writer.write('.notNullable()');
619
- }
620
- } else {
621
- if (field.nonNull) {
622
- this.writer.write(`.notNullable()`);
623
- } else {
624
- this.writer.write('.nullable()');
733
+ return false;
625
734
  }
735
+
736
+ return true;
626
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()');
627
779
  }
628
780
  if (setDefault && field.defaultValue !== undefined) {
629
781
  this.writer.write(`.defaultTo(${this.value(field.defaultValue)})`);
@@ -638,7 +790,6 @@ export class MigrationGenerator {
638
790
  }
639
791
  this.writer.write(';').newLine();
640
792
  };
641
- const kind = field.kind;
642
793
  switch (kind) {
643
794
  case undefined:
644
795
  case 'primitive':
@@ -712,6 +863,44 @@ export class MigrationGenerator {
712
863
  private getColumn(tableName: string, columnName: string) {
713
864
  return this.columns[tableName].find((col) => col.name === columnName);
714
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
+ }
715
904
  }
716
905
 
717
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;