@smartive/graphql-magic 22.3.0 → 22.5.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/CHANGELOG.md +3 -3
- package/dist/bin/gqm.cjs +190 -33
- package/dist/cjs/index.cjs +209 -41
- package/dist/esm/migrations/generate.d.ts +4 -0
- package/dist/esm/migrations/generate.js +180 -35
- package/dist/esm/migrations/generate.js.map +1 -1
- package/dist/esm/models/model-definitions.d.ts +1 -0
- package/dist/esm/permissions/check.js +12 -5
- package/dist/esm/permissions/check.js.map +1 -1
- package/package.json +4 -4
- package/src/migrations/generate.ts +220 -35
- package/src/models/model-definitions.ts +1 -0
- package/src/permissions/check.ts +22 -8
|
@@ -160,35 +160,27 @@ export class MigrationGenerator {
|
|
|
160
160
|
);
|
|
161
161
|
|
|
162
162
|
// Update fields
|
|
163
|
-
const
|
|
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 (
|
|
173
|
+
if (col.generation_expression !== field.generateAs) {
|
|
170
174
|
return true;
|
|
171
175
|
}
|
|
172
176
|
|
|
173
|
-
|
|
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
|
|
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
|
-
|
|
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 = () => {
|
package/src/permissions/check.ts
CHANGED
|
@@ -152,7 +152,7 @@ export const checkCanWrite = async (
|
|
|
152
152
|
throw new PermissionError(getRole(ctx), action, model.plural, 'no applicable permissions');
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
const query = ctx.knex.
|
|
155
|
+
const query = ctx.knex.first();
|
|
156
156
|
let linked = false;
|
|
157
157
|
|
|
158
158
|
for (const field of model.fields.filter(
|
|
@@ -183,7 +183,9 @@ export const checkCanWrite = async (
|
|
|
183
183
|
if (fieldPermissionStack === true) {
|
|
184
184
|
// User can link any entity from this type, just check whether it exists
|
|
185
185
|
|
|
186
|
-
query.
|
|
186
|
+
query.select(
|
|
187
|
+
ctx.knex.raw(`EXISTS(SELECT 1 FROM ?? as a WHERE a.id = ?) as ??`, [field.type, foreignId, foreignKey]),
|
|
188
|
+
);
|
|
187
189
|
continue;
|
|
188
190
|
}
|
|
189
191
|
|
|
@@ -196,10 +198,16 @@ export const checkCanWrite = async (
|
|
|
196
198
|
);
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
query.select(
|
|
202
|
+
ctx.knex.raw(
|
|
203
|
+
`${fieldPermissionStack
|
|
204
|
+
.map((links) => {
|
|
205
|
+
const subQuery = ctx.knex.queryBuilder();
|
|
206
|
+
permissionLinkQuery(ctx, subQuery, links, foreignId);
|
|
207
|
+
|
|
208
|
+
return `EXISTS(${subQuery.toString()})`;
|
|
209
|
+
})
|
|
210
|
+
.join(' OR ')} as "${foreignKey}"`,
|
|
203
211
|
),
|
|
204
212
|
);
|
|
205
213
|
}
|
|
@@ -211,8 +219,14 @@ export const checkCanWrite = async (
|
|
|
211
219
|
console.debug('QUERY', query.toString());
|
|
212
220
|
}
|
|
213
221
|
const canMutate = await query;
|
|
214
|
-
|
|
215
|
-
|
|
222
|
+
const cannotLink = Object.entries(canMutate).filter(([, value]) => !value);
|
|
223
|
+
if (cannotLink.length) {
|
|
224
|
+
throw new PermissionError(
|
|
225
|
+
role,
|
|
226
|
+
action,
|
|
227
|
+
`this ${model.name}`,
|
|
228
|
+
`cannot link to ${cannotLink.map(([key]) => `${key}: ${data[key]}`).join(', ')}`,
|
|
229
|
+
);
|
|
216
230
|
}
|
|
217
231
|
} else if (action === 'CREATE') {
|
|
218
232
|
throw new PermissionError(role, action, `this ${model.name}`, 'no linkable entities');
|