@rvoh/dream 2.19.0 → 2.21.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.
- package/dist/cjs/src/Dream.js +83 -0
- package/dist/cjs/src/cli/index.js +7 -4
- package/dist/cjs/src/dream/Query.js +11 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +62 -14
- package/dist/cjs/src/dream/internal/filterRowToKnownColumns.js +41 -0
- package/dist/cjs/src/dream/internal/saveDream.js +6 -1
- package/dist/cjs/src/dream/internal/sqlResultToDreamInstance.js +9 -2
- package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/cjs/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
- package/dist/cjs/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
- package/dist/cjs/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
- package/dist/cjs/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
- package/dist/cjs/src/helpers/cli/ASTConnectionBuilder.js +38 -1
- package/dist/cjs/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
- package/dist/cjs/src/helpers/cli/generateMigration.js +20 -3
- package/dist/cjs/src/helpers/cli/generateMigrationContent.js +74 -9
- package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +198 -0
- package/dist/esm/src/Dream.js +83 -0
- package/dist/esm/src/cli/index.js +7 -4
- package/dist/esm/src/dream/Query.js +11 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +62 -14
- package/dist/esm/src/dream/internal/filterRowToKnownColumns.js +41 -0
- package/dist/esm/src/dream/internal/saveDream.js +6 -1
- package/dist/esm/src/dream/internal/sqlResultToDreamInstance.js +9 -2
- package/dist/esm/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/esm/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreAssociationColumn.js +38 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreEncryptedColumn.js +20 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnorePrimaryKey.js +18 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.js +21 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.js +20 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.js +24 -0
- package/dist/esm/src/errors/schema-builder/CannotIgnoreStiTypeColumn.js +17 -0
- package/dist/esm/src/errors/schema-builder/ConflictingIgnoredColumns.js +27 -0
- package/dist/esm/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.js +21 -0
- package/dist/esm/src/helpers/cli/ASTConnectionBuilder.js +38 -1
- package/dist/esm/src/helpers/cli/ASTKyselyCodegenEnhancer.js +60 -0
- package/dist/esm/src/helpers/cli/generateMigration.js +20 -3
- package/dist/esm/src/helpers/cli/generateMigrationContent.js +74 -9
- package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +198 -0
- package/dist/types/src/Dream.d.ts +81 -0
- package/dist/types/src/cli/index.d.ts +1 -1
- package/dist/types/src/dream/Query.d.ts +11 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +21 -0
- package/dist/types/src/dream/internal/filterRowToKnownColumns.d.ts +30 -0
- package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +6 -0
- package/dist/types/src/errors/UnparseableMigrationColumn.d.ts +5 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreAssociationColumn.d.ts +12 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreEncryptedColumn.d.ts +7 -0
- package/dist/types/src/errors/schema-builder/CannotIgnorePrimaryKey.d.ts +6 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreSoftDeleteColumn.d.ts +7 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreSortablePositionColumn.d.ts +7 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreSortableScopeColumn.d.ts +8 -0
- package/dist/types/src/errors/schema-builder/CannotIgnoreStiTypeColumn.d.ts +6 -0
- package/dist/types/src/errors/schema-builder/ConflictingIgnoredColumns.d.ts +7 -0
- package/dist/types/src/errors/schema-builder/IgnoredColumnMustBeCamelCase.d.ts +7 -0
- package/dist/types/src/helpers/cli/ASTConnectionBuilder.d.ts +20 -0
- package/dist/types/src/helpers/cli/ASTKyselyCodegenEnhancer.d.ts +13 -0
- package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +11 -1
- package/dist/types/src/helpers/cli/resolveIgnoredColumns.d.ts +41 -0
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +57 -39
- package/docs/classes/db.PostgresQueryDriver.html +58 -40
- package/docs/classes/db.QueryDriverBase.html +38 -38
- package/docs/classes/errors.CheckConstraintViolation.html +3 -3
- package/docs/classes/errors.ColumnOverflow.html +3 -3
- package/docs/classes/errors.CreateOrFindByFailedToCreateAndFind.html +3 -3
- package/docs/classes/errors.DataIncompatibleWithDatabaseField.html +3 -3
- package/docs/classes/errors.DataTypeColumnTypeMismatch.html +3 -3
- package/docs/classes/errors.DecryptionError.html +2 -2
- package/docs/classes/errors.DecryptionParseError.html +2 -2
- package/docs/classes/errors.DecryptionRotationError.html +3 -3
- package/docs/classes/errors.GlobalNameNotSet.html +3 -3
- package/docs/classes/errors.InvalidCalendarDate.html +2 -2
- package/docs/classes/errors.InvalidClockTime.html +2 -2
- package/docs/classes/errors.InvalidClockTimeTz.html +2 -2
- package/docs/classes/errors.InvalidDateTime.html +2 -2
- package/docs/classes/errors.MissingSerializersDefinition.html +3 -3
- package/docs/classes/errors.NonLoadedAssociation.html +3 -3
- package/docs/classes/errors.NotNullViolation.html +3 -3
- package/docs/classes/errors.RecordNotFound.html +3 -3
- package/docs/classes/errors.ValidationError.html +3 -3
- package/docs/classes/index.CalendarDate.html +33 -33
- package/docs/classes/index.ClockTime.html +32 -32
- package/docs/classes/index.ClockTimeTz.html +35 -35
- package/docs/classes/index.DateTime.html +86 -86
- package/docs/classes/index.Decorators.html +19 -19
- package/docs/classes/index.Dream.html +188 -123
- package/docs/classes/index.DreamApp.html +10 -10
- package/docs/classes/index.DreamTransaction.html +2 -2
- package/docs/classes/index.Env.html +2 -2
- package/docs/classes/index.Query.html +73 -62
- package/docs/classes/system.CliFileWriter.html +4 -4
- package/docs/classes/system.DreamBin.html +2 -2
- package/docs/classes/system.DreamCLI.html +7 -7
- package/docs/classes/system.DreamImporter.html +2 -2
- package/docs/classes/system.DreamLogos.html +2 -2
- package/docs/classes/system.DreamSerializerBuilder.html +11 -11
- package/docs/classes/system.ObjectSerializerBuilder.html +8 -8
- package/docs/classes/system.PathHelpers.html +3 -3
- package/docs/classes/utils.Encrypt.html +3 -3
- package/docs/classes/utils.Range.html +2 -2
- package/docs/functions/db.closeAllDbConnections.html +1 -1
- package/docs/functions/db.dreamDbConnections.html +1 -1
- package/docs/functions/db.untypedDb.html +1 -1
- package/docs/functions/db.validateColumn.html +1 -1
- package/docs/functions/db.validateTable.html +1 -1
- package/docs/functions/errors.pgErrorType.html +1 -1
- package/docs/functions/index.DreamSerializer.html +1 -1
- package/docs/functions/index.ObjectSerializer.html +1 -1
- package/docs/functions/index.ReplicaSafe.html +1 -1
- package/docs/functions/index.STI.html +1 -1
- package/docs/functions/index.SoftDelete.html +1 -1
- package/docs/functions/utils.camelize.html +1 -1
- package/docs/functions/utils.capitalize.html +1 -1
- package/docs/functions/utils.cloneDeepSafe.html +1 -1
- package/docs/functions/utils.compact.html +1 -1
- package/docs/functions/utils.groupBy.html +1 -1
- package/docs/functions/utils.hyphenize.html +1 -1
- package/docs/functions/utils.intersection.html +1 -1
- package/docs/functions/utils.isEmpty.html +1 -1
- package/docs/functions/utils.normalizeUnicode.html +1 -1
- package/docs/functions/utils.pascalize.html +1 -1
- package/docs/functions/utils.percent.html +1 -1
- package/docs/functions/utils.range.html +1 -1
- package/docs/functions/utils.round.html +1 -1
- package/docs/functions/utils.sanitizeString.html +1 -1
- package/docs/functions/utils.snakeify.html +1 -1
- package/docs/functions/utils.sort.html +1 -1
- package/docs/functions/utils.sortBy.html +1 -1
- package/docs/functions/utils.sortObjectByKey.html +1 -1
- package/docs/functions/utils.sortObjectByValue.html +1 -1
- package/docs/functions/utils.uncapitalize.html +1 -1
- package/docs/functions/utils.uniq.html +1 -1
- package/docs/interfaces/openapi.OpenapiDescription.html +2 -2
- package/docs/interfaces/openapi.OpenapiSchemaProperties.html +1 -1
- package/docs/interfaces/openapi.OpenapiSchemaPropertiesShorthand.html +1 -1
- package/docs/interfaces/openapi.OpenapiTypeFieldObject.html +1 -1
- package/docs/interfaces/types.BelongsToStatement.html +2 -2
- package/docs/interfaces/types.DecoratorContext.html +2 -2
- package/docs/interfaces/types.DreamAppInitOptions.html +2 -2
- package/docs/interfaces/types.DreamAppOpts.html +2 -2
- package/docs/interfaces/types.DreamDbConfig.html +5 -5
- package/docs/interfaces/types.DurationObject.html +2 -2
- package/docs/interfaces/types.EncryptOptions.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersMany.html +2 -2
- package/docs/interfaces/types.InternalAnyTypedSerializerRendersOne.html +2 -2
- package/docs/interfaces/types.SerializerRendererOpts.html +2 -2
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ import intersection from '../intersection.js';
|
|
|
11
11
|
import sortBy from '../sortBy.js';
|
|
12
12
|
import uniq from '../uniq.js';
|
|
13
13
|
import ASTBuilder from './ASTBuilder.js';
|
|
14
|
+
import resolveIgnoredColumns from './resolveIgnoredColumns.js';
|
|
14
15
|
/**
|
|
15
16
|
* @internal
|
|
16
17
|
*
|
|
@@ -265,12 +266,48 @@ may need to update the table getter in the corresponding Dream.
|
|
|
265
266
|
default: uniq(models.flatMap(model => model['scopes'].default.map(scopeStatement => scopeStatement.method))),
|
|
266
267
|
named: uniq(models.flatMap(model => model['scopes'].named.map(scopeStatement => scopeStatement.method))),
|
|
267
268
|
},
|
|
268
|
-
columns: await this.getColumnData(tableName, associationData),
|
|
269
|
+
columns: this.withoutIgnoredColumns(await this.getColumnData(tableName, associationData), tableName),
|
|
269
270
|
virtualColumns: uniq(models.flatMap(model => model['virtualAttributes'].map(prop => prop.property) || [])),
|
|
270
271
|
associations: associationData,
|
|
271
272
|
serializerKeys,
|
|
272
273
|
};
|
|
273
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* @internal
|
|
277
|
+
*
|
|
278
|
+
* resolves the ignored columns declared by the models backed by the
|
|
279
|
+
* given table (validating the declarations; see resolveIgnoredColumns)
|
|
280
|
+
*/
|
|
281
|
+
ignoredColumnsForTable(tableName) {
|
|
282
|
+
const dreamApp = DreamApp.getOrFail();
|
|
283
|
+
const allModels = Object.values(dreamApp.models).filter(model => model.prototype?.connectionName === this.connectionName);
|
|
284
|
+
const models = allModels.filter(model => model.table === tableName);
|
|
285
|
+
return resolveIgnoredColumns(models, tableName, allModels);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* @internal
|
|
289
|
+
*
|
|
290
|
+
* returns the provided column data without the columns that the table's
|
|
291
|
+
* models declare in ignoredColumns. This is what removes ignored columns
|
|
292
|
+
* from the generated dream schema file: `columns()` reads the generated
|
|
293
|
+
* schema at runtime, so every column enumeration built from `columns()`
|
|
294
|
+
* (preload and join-load select lists, save hydration, attribute
|
|
295
|
+
* definition) inherits this filtering. A column that is ignored but not
|
|
296
|
+
* present in the introspected table (e.g. after the drop migration has
|
|
297
|
+
* run but before the declaration is removed) is a no-op.
|
|
298
|
+
*/
|
|
299
|
+
withoutIgnoredColumns(columnData, tableName) {
|
|
300
|
+
const ignoredColumns = this.ignoredColumnsForTable(tableName);
|
|
301
|
+
if (!ignoredColumns.size)
|
|
302
|
+
return columnData;
|
|
303
|
+
return Object.keys(columnData)
|
|
304
|
+
.filter(columnName => !ignoredColumns.has(columnName))
|
|
305
|
+
.reduce((filtered, columnName) => {
|
|
306
|
+
;
|
|
307
|
+
filtered[columnName] = columnData[columnName];
|
|
308
|
+
return filtered;
|
|
309
|
+
}, {});
|
|
310
|
+
}
|
|
274
311
|
/**
|
|
275
312
|
* @internal
|
|
276
313
|
*
|
|
@@ -25,6 +25,7 @@ export default class ASTKyselyCodegenEnhancer extends ASTConnectionBuilder {
|
|
|
25
25
|
async enhance() {
|
|
26
26
|
let dbSourceFile = await this.getDbSourceFile();
|
|
27
27
|
dbSourceFile = this.camelizeKeys(dbSourceFile);
|
|
28
|
+
dbSourceFile = this.removeIgnoredColumns(dbSourceFile);
|
|
28
29
|
dbSourceFile = this.replaceTimestampExport(dbSourceFile);
|
|
29
30
|
dbSourceFile = await this.replaceTimeFieldsInInterfaces(dbSourceFile);
|
|
30
31
|
dbSourceFile = this.addMissingImports(dbSourceFile);
|
|
@@ -283,6 +284,65 @@ ${output}`);
|
|
|
283
284
|
const result = ts.transform(dbSourceFile, [transformer]);
|
|
284
285
|
return result.transformed[0];
|
|
285
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* @internal
|
|
289
|
+
*
|
|
290
|
+
* removes columns declared in model `ignoredColumns` getters from the
|
|
291
|
+
* table interfaces generated by kysely-codegen. This is what removes
|
|
292
|
+
* ignored columns from the generated db types file: every column-name
|
|
293
|
+
* level type (`DreamColumnNames`, `UpdateableProperties`, where-clause
|
|
294
|
+
* statements) derives from the Kysely `DB` interface, so filtering here
|
|
295
|
+
* turns any remaining code reference to an ignored column into a type
|
|
296
|
+
* error. Runs after camelizeKeys so that member names align with the
|
|
297
|
+
* camelCase column names models declare in ignoredColumns.
|
|
298
|
+
*/
|
|
299
|
+
removeIgnoredColumns(dbSourceFile) {
|
|
300
|
+
const dbInterface = this.findDbExport(dbSourceFile, 'DB');
|
|
301
|
+
if (!dbInterface)
|
|
302
|
+
return dbSourceFile;
|
|
303
|
+
// the DB interface maps table names to table interfaces (e.g.
|
|
304
|
+
// `collars: Collars`), so it provides the table name each table
|
|
305
|
+
// interface corresponds to
|
|
306
|
+
const ignoredColumnsByInterfaceName = {};
|
|
307
|
+
dbInterface.members.forEach(member => {
|
|
308
|
+
if (!ts.isPropertySignature(member))
|
|
309
|
+
return;
|
|
310
|
+
if (!member.type || !ts.isTypeReferenceNode(member.type))
|
|
311
|
+
return;
|
|
312
|
+
if (!ts.isIdentifier(member.type.typeName))
|
|
313
|
+
return;
|
|
314
|
+
const tableName = ts.isIdentifier(member.name)
|
|
315
|
+
? member.name.text
|
|
316
|
+
: ts.isStringLiteral(member.name)
|
|
317
|
+
? member.name.text
|
|
318
|
+
: null;
|
|
319
|
+
if (!tableName)
|
|
320
|
+
return;
|
|
321
|
+
const ignoredColumns = this.ignoredColumnsForTable(tableName);
|
|
322
|
+
if (ignoredColumns.size)
|
|
323
|
+
ignoredColumnsByInterfaceName[member.type.typeName.text] = ignoredColumns;
|
|
324
|
+
});
|
|
325
|
+
if (!Object.keys(ignoredColumnsByInterfaceName).length)
|
|
326
|
+
return dbSourceFile;
|
|
327
|
+
// @ts-expect-error cannot lock this type down, though implementation is correct
|
|
328
|
+
const transformer = context => {
|
|
329
|
+
const visit = node => {
|
|
330
|
+
const interfaceNode = this.exportedInterfaceOrNull(node);
|
|
331
|
+
const ignoredColumns = interfaceNode
|
|
332
|
+
? ignoredColumnsByInterfaceName[interfaceNode.name.text]
|
|
333
|
+
: undefined;
|
|
334
|
+
if (interfaceNode && ignoredColumns) {
|
|
335
|
+
return f.updateInterfaceDeclaration(interfaceNode, interfaceNode.modifiers, interfaceNode.name, interfaceNode.typeParameters, interfaceNode.heritageClauses, interfaceNode.members.filter(member => !(ts.isPropertySignature(member) &&
|
|
336
|
+
(ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) &&
|
|
337
|
+
ignoredColumns.has(member.name.text))));
|
|
338
|
+
}
|
|
339
|
+
return ts.visitEachChild(node, visit, context);
|
|
340
|
+
};
|
|
341
|
+
return node => ts.visitNode(node, visit);
|
|
342
|
+
};
|
|
343
|
+
const result = ts.transform(dbSourceFile, [transformer]);
|
|
344
|
+
return result.transformed[0];
|
|
345
|
+
}
|
|
286
346
|
/**
|
|
287
347
|
* @internal
|
|
288
348
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
2
|
import pluralize from 'pluralize-esm';
|
|
3
|
-
import generateMigrationContent from '../cli/generateMigrationContent.js';
|
|
3
|
+
import generateMigrationContent, { MIGRATION_TABLE_NAME_PLACEHOLDER, } from '../cli/generateMigrationContent.js';
|
|
4
4
|
import primaryKeyType from '../db/primaryKeyType.js';
|
|
5
5
|
import hyphenize from '../hyphenize.js';
|
|
6
6
|
import migrationVersion from '../migrationVersion.js';
|
|
@@ -9,6 +9,18 @@ import dreamPath from '../path/dreamPath.js';
|
|
|
9
9
|
import snakeify from '../snakeify.js';
|
|
10
10
|
import generateStiMigrationContent from './generateStiMigrationContent.js';
|
|
11
11
|
import writeGeneratedFile from './writeGeneratedFile.js';
|
|
12
|
+
/**
|
|
13
|
+
* Equivalent to `migrationName.match(new RegExp(`${marker}(.+)$`))?.[1]`, but
|
|
14
|
+
* without a regex: CodeQL flags `.+` anchored to `$` as a polynomial-ReDoS
|
|
15
|
+
* shape, and a plain `indexOf`/`slice` does the same leftmost-match-to-end
|
|
16
|
+
* job with no backtracking risk.
|
|
17
|
+
*/
|
|
18
|
+
function tableNameFromSuffix(migrationName, marker) {
|
|
19
|
+
const markerIndex = migrationName.indexOf(marker);
|
|
20
|
+
if (markerIndex === -1)
|
|
21
|
+
return undefined;
|
|
22
|
+
return migrationName.slice(markerIndex + marker.length) || undefined;
|
|
23
|
+
}
|
|
12
24
|
export default async function generateMigration({ migrationName, columnsWithTypes, connectionName, fullyQualifiedModelName, fullyQualifiedParentName, tableName: explicitTableName, modelClassName, softDelete = false, }) {
|
|
13
25
|
const migrationsBasePath = connectionName === 'default'
|
|
14
26
|
? path.join(dreamPath('db'), 'migrations')
|
|
@@ -33,12 +45,17 @@ export default async function generateMigration({ migrationName, columnsWithType
|
|
|
33
45
|
});
|
|
34
46
|
}
|
|
35
47
|
else {
|
|
36
|
-
const
|
|
48
|
+
const toTableName = tableNameFromSuffix(migrationName, '-to-');
|
|
49
|
+
const fromTableName = toTableName
|
|
50
|
+
? undefined
|
|
51
|
+
: tableNameFromSuffix(migrationName, '-from-');
|
|
52
|
+
const tableName = toTableName || fromTableName;
|
|
37
53
|
content = generateMigrationContent({
|
|
38
|
-
table: tableName ? pluralize(snakeify(tableName)) :
|
|
54
|
+
table: tableName ? pluralize(snakeify(tableName)) : MIGRATION_TABLE_NAME_PLACEHOLDER,
|
|
39
55
|
columnsWithTypes,
|
|
40
56
|
primaryKeyType: primaryKeyType(connectionName),
|
|
41
57
|
createOrAlter: 'alter',
|
|
58
|
+
alterDirection: fromTableName ? 'remove' : 'add',
|
|
42
59
|
});
|
|
43
60
|
}
|
|
44
61
|
await writeGeneratedFile({
|
|
@@ -2,11 +2,19 @@ import pluralize from 'pluralize-esm';
|
|
|
2
2
|
import lookupModelByGlobalName from '../../dream-app/helpers/lookupModelByGlobalName.js';
|
|
3
3
|
import Query from '../../dream/Query.js';
|
|
4
4
|
import InvalidDecimalFieldPassedToGenerator from '../../errors/InvalidDecimalFieldPassedToGenerator.js';
|
|
5
|
+
import NoColumnsToAlterMigration from '../../errors/NoColumnsToAlterMigration.js';
|
|
6
|
+
import UnparseableMigrationColumn from '../../errors/UnparseableMigrationColumn.js';
|
|
5
7
|
import camelize from '../camelize.js';
|
|
6
8
|
import compact from '../compact.js';
|
|
7
9
|
import globalClassNameFromFullyQualifiedModelName from '../globalClassNameFromFullyQualifiedModelName.js';
|
|
8
10
|
import snakeify from '../snakeify.js';
|
|
9
11
|
import standardizeFullyQualifiedModelName from '../standardizeFullyQualifiedModelName.js';
|
|
12
|
+
// Sentinel table name used by generateMigration.ts when a standalone
|
|
13
|
+
// migration name matches neither a `-to-<table>` nor `-from-<table>` suffix.
|
|
14
|
+
// That path intentionally produces a stub the user is expected to hand-edit
|
|
15
|
+
// (e.g. an index-only migration with no column operations at all), so it's
|
|
16
|
+
// exempt from the "no valid columns" check below.
|
|
17
|
+
export const MIGRATION_TABLE_NAME_PLACEHOLDER = '<table-name>';
|
|
10
18
|
const STI_TYPE_COLUMN_NAME = 'type';
|
|
11
19
|
// deleted_at is deliberately NOT in this list: the SoftDelete default scope's
|
|
12
20
|
// `WHERE deleted_at IS NULL` is unselective on healthy tables, Dream internals
|
|
@@ -15,8 +23,9 @@ const STI_TYPE_COLUMN_NAME = 'type';
|
|
|
15
23
|
// default. See spec/unit/cli/generateMigrationContent.spec.ts
|
|
16
24
|
// ("deleted_at is deliberately NOT indexed") and the CHANGELOG.
|
|
17
25
|
const COLUMNS_TO_INDEX = [STI_TYPE_COLUMN_NAME];
|
|
18
|
-
export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', stiChildClassName, softDelete = false, } = {}) {
|
|
26
|
+
export default function generateMigrationContent({ connectionName = 'default', table, columnsWithTypes = [], primaryKeyType = 'bigserial', createOrAlter = 'create', alterDirection = 'add', stiChildClassName, softDelete = false, } = {}) {
|
|
19
27
|
const altering = createOrAlter === 'alter';
|
|
28
|
+
const removingInUp = altering && alterDirection === 'remove';
|
|
20
29
|
let requireCitextExtension = false;
|
|
21
30
|
const checkConstraints = [];
|
|
22
31
|
// When creating a new table, we automatically emit `created_at`,
|
|
@@ -65,7 +74,17 @@ export default function generateMigrationContent({ connectionName = 'default', t
|
|
|
65
74
|
// when creating a migration for an STI child, we don't want to include notNull;
|
|
66
75
|
// instead, we'll add a check constraint that uses the STI child class name
|
|
67
76
|
const sqlAttributeType = getAttributeType(attributeType, descriptors);
|
|
68
|
-
if (attributeType === undefined
|
|
77
|
+
if (attributeType === undefined) {
|
|
78
|
+
// In alter mode (both `-to-`/add and `-from-`/remove), a column whose
|
|
79
|
+
// type can't be resolved (e.g. a mistyped declaration with no `:type`
|
|
80
|
+
// segment) must not silently vanish from the generated migration —
|
|
81
|
+
// that's especially dangerous for `-from-` migrations, whose whole
|
|
82
|
+
// premise is describing removed columns so `down` can restore them.
|
|
83
|
+
if (altering)
|
|
84
|
+
throw new UnparseableMigrationColumn(attributeDeclaration);
|
|
85
|
+
return acc;
|
|
86
|
+
}
|
|
87
|
+
if (['hasone', 'hasmany'].includes(processedAttrType))
|
|
69
88
|
return acc;
|
|
70
89
|
if (attributeType === 'citext')
|
|
71
90
|
requireCitextExtension = true;
|
|
@@ -169,6 +188,25 @@ export async function down(db: Kysely<any>): Promise<void> {
|
|
|
169
188
|
}\
|
|
170
189
|
`;
|
|
171
190
|
}
|
|
191
|
+
// An alter migration (either `-to-`/add or `-from-`/remove) with no valid
|
|
192
|
+
// columns to add/drop would otherwise emit a bare `.alterTable(...).execute()`
|
|
193
|
+
// with no column operation at all, which Postgres rejects at migration-run
|
|
194
|
+
// time rather than generation time. Fail loudly here instead.
|
|
195
|
+
//
|
|
196
|
+
// This guard is scoped to the standalone `g:migration` `-to-`/`-from-` flow
|
|
197
|
+
// only (`stiChildClassName` is unset there). `g:sti-child` legitimately
|
|
198
|
+
// calls this function in alter mode with zero `columnsWithTypes` — a
|
|
199
|
+
// zero-attribute STI child is a documented use case (see `g:sti-child`'s
|
|
200
|
+
// help example in src/cli/index.ts), and `generateStiMigrationContent`
|
|
201
|
+
// handles the type-column/check-constraint machinery separately from the
|
|
202
|
+
// `columnDefs` this check inspects, so an empty `columnDefs` there is
|
|
203
|
+
// expected, not a sign of a mistyped/unparseable column list.
|
|
204
|
+
if (altering &&
|
|
205
|
+
columnDefs.length === 0 &&
|
|
206
|
+
table !== MIGRATION_TABLE_NAME_PLACEHOLDER &&
|
|
207
|
+
!stiChildClassName) {
|
|
208
|
+
throw new NoColumnsToAlterMigration(table, alterDirection);
|
|
209
|
+
}
|
|
172
210
|
const citextExtension = requireCitextExtension
|
|
173
211
|
? ` await DreamMigrationHelpers.createExtension(db, 'citext')\n\n`
|
|
174
212
|
: '';
|
|
@@ -179,7 +217,22 @@ export async function down(db: Kysely<any>): Promise<void> {
|
|
|
179
217
|
const newlineIndent = '\n ';
|
|
180
218
|
const newlineDoubleIndent = '\n ';
|
|
181
219
|
const doubleNewlineIndent = '\n\n ';
|
|
182
|
-
|
|
220
|
+
// For a `-from-` migration (`alterDirection: 'remove'`), `down` re-adds the
|
|
221
|
+
// column(s) that `up` removed. The generator has no way to know the
|
|
222
|
+
// column's original default, so a non-optional (`.notNull()`) column with
|
|
223
|
+
// no default emitted here will fail at migration-run time against a table
|
|
224
|
+
// that already has rows. We can't fix that without a lot more heavy
|
|
225
|
+
// lifting (and the risk that comes with it), so flag it with a comment
|
|
226
|
+
// instead of silently generating a `down` that's likely to break.
|
|
227
|
+
const NO_KNOWN_DEFAULT_COMMENT = "// NOTE: the generator doesn't know this column's original default; this addColumn will fail against a table with existing rows unless you add a default by hand.";
|
|
228
|
+
const finalColumnDefs = removingInUp
|
|
229
|
+
? columnDefs.map(def => def.includes('.notNull()') && !def.includes('.defaultTo(')
|
|
230
|
+
? `${NO_KNOWN_DEFAULT_COMMENT}\n ${def}`
|
|
231
|
+
: def)
|
|
232
|
+
: columnDefs;
|
|
233
|
+
const columnDefLines = finalColumnDefs.length
|
|
234
|
+
? newlineDoubleIndent + finalColumnDefs.join(newlineDoubleIndent)
|
|
235
|
+
: '';
|
|
183
236
|
const columnDropLines = columnDrops.length
|
|
184
237
|
? newlineDoubleIndent + columnDrops.join(newlineDoubleIndent) + newlineDoubleIndent
|
|
185
238
|
: '';
|
|
@@ -190,21 +243,33 @@ export async function down(db: Kysely<any>): Promise<void> {
|
|
|
190
243
|
newlineDoubleIndent +
|
|
191
244
|
".addColumn('updated_at', 'timestamp', col => col.notNull())" +
|
|
192
245
|
(emitDeletedAtColumn ? newlineDoubleIndent + ".addColumn('deleted_at', 'timestamp')" : '');
|
|
246
|
+
// For `alterDirection: 'remove'` (a `-from-` migration), the enum *type*
|
|
247
|
+
// itself must never be created or dropped here, even when the column
|
|
248
|
+
// declaration includes inline enum values — enums are frequently reused
|
|
249
|
+
// across columns/tables, so `up` must drop only the column (no `dropType`)
|
|
250
|
+
// and `down` must re-add only the column (no `createType`), identical to
|
|
251
|
+
// referencing an existing enum by name with no values.
|
|
252
|
+
const enumCreateStatements = removingInUp ? '' : generateEnumStatements(columnsWithTypes);
|
|
253
|
+
const enumDropStatements = removingInUp ? '' : generateEnumDropStatements(columnsWithTypes);
|
|
254
|
+
const addColumnsBody = `${citextExtension}${enumCreateStatements} await db.schema
|
|
255
|
+
.${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${timestampColumnLines}
|
|
256
|
+
.execute()${indexDefs.length ? `\n${newlineIndent}` : ''}${indexDefs.join(doubleNewlineIndent)}${checkConstraints.join('')}`;
|
|
257
|
+
const removeColumnsBody = ` ${altering
|
|
258
|
+
? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
|
|
259
|
+
: `await db.schema.dropTable('${table}').execute()`}${enumDropStatements}`;
|
|
260
|
+
const upBody = removingInUp ? removeColumnsBody : addColumnsBody;
|
|
261
|
+
const downBody = removingInUp ? addColumnsBody : removeColumnsBody;
|
|
193
262
|
return `\
|
|
194
263
|
${dreamDbImports.length ? `import { ${dreamDbImports.join(', ')} } from '@rvoh/dream/db'\n` : ''}import { ${kyselyImports.join(', ')} } from 'kysely'
|
|
195
264
|
|
|
196
265
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
197
266
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
198
|
-
${
|
|
199
|
-
.${altering ? 'alterTable' : 'createTable'}('${table}')${altering ? '' : newlineDoubleIndent + generateIdStr({ primaryKeyType })}${columnDefLines}${timestampColumnLines}
|
|
200
|
-
.execute()${indexDefs.length ? `\n${newlineIndent}` : ''}${indexDefs.join(doubleNewlineIndent)}${checkConstraints.join('')}
|
|
267
|
+
${upBody}
|
|
201
268
|
}
|
|
202
269
|
|
|
203
270
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
271
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
205
|
-
|
|
206
|
-
? `await db.schema${newlineDoubleIndent}.alterTable('${table}')${columnDropLines}.execute()`
|
|
207
|
-
: `await db.schema.dropTable('${table}').execute()`}${generateEnumDropStatements(columnsWithTypes)}
|
|
272
|
+
${downBody}
|
|
208
273
|
}\
|
|
209
274
|
`;
|
|
210
275
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import scopeArray from '../../decorators/field/sortable/helpers/scopeArray.js';
|
|
2
|
+
import CannotIgnoreAssociationColumn from '../../errors/schema-builder/CannotIgnoreAssociationColumn.js';
|
|
3
|
+
import CannotIgnoreEncryptedColumn from '../../errors/schema-builder/CannotIgnoreEncryptedColumn.js';
|
|
4
|
+
import CannotIgnorePrimaryKey from '../../errors/schema-builder/CannotIgnorePrimaryKey.js';
|
|
5
|
+
import CannotIgnoreSoftDeleteColumn from '../../errors/schema-builder/CannotIgnoreSoftDeleteColumn.js';
|
|
6
|
+
import CannotIgnoreSortablePositionColumn from '../../errors/schema-builder/CannotIgnoreSortablePositionColumn.js';
|
|
7
|
+
import CannotIgnoreSortableScopeColumn from '../../errors/schema-builder/CannotIgnoreSortableScopeColumn.js';
|
|
8
|
+
import CannotIgnoreStiTypeColumn from '../../errors/schema-builder/CannotIgnoreStiTypeColumn.js';
|
|
9
|
+
import ConflictingIgnoredColumns from '../../errors/schema-builder/ConflictingIgnoredColumns.js';
|
|
10
|
+
import IgnoredColumnMustBeCamelCase from '../../errors/schema-builder/IgnoredColumnMustBeCamelCase.js';
|
|
11
|
+
import camelize from '../camelize.js';
|
|
12
|
+
import uniq from '../uniq.js';
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*
|
|
16
|
+
* Resolves the set of ignored columns for a table from the `ignoredColumns`
|
|
17
|
+
* declarations of every model backed by that table, validating the
|
|
18
|
+
* declarations along the way. Called while `sync` builds the generated types
|
|
19
|
+
* files (the declarations have no runtime behavior; see the `ignoredColumns`
|
|
20
|
+
* getter on Dream), so each of these guards fails the sync command loudly
|
|
21
|
+
* rather than surfacing as broken behavior at runtime:
|
|
22
|
+
*
|
|
23
|
+
* - ignored columns must be declared in camelCase, since generated column
|
|
24
|
+
* names are camelized, so any other shape can never match a generated
|
|
25
|
+
* column and would be silently inert
|
|
26
|
+
* - a model may never ignore its primary key
|
|
27
|
+
* - an STI model may never ignore the STI "type" column
|
|
28
|
+
* - a model may never ignore an @Sortable position field or a plain-column
|
|
29
|
+
* @Sortable scope, the backing column of an @Encrypted property, or (on a
|
|
30
|
+
* SoftDelete model) its deletedAtField — the framework reads and writes
|
|
31
|
+
* those columns by name
|
|
32
|
+
* - models sharing a table must agree on their ignored columns, since there
|
|
33
|
+
* is only one generated schema per table (STI children inherit the base
|
|
34
|
+
* model's getter, so agreement is automatic unless a child overrides it)
|
|
35
|
+
* - no association anywhere in the app may name an ignored column as the
|
|
36
|
+
* foreign key (or polymorphic type field) it reads and writes on this
|
|
37
|
+
* table — a BelongsTo on a model backed by this table names a foreign key
|
|
38
|
+
* on this table, and so does a HasMany/HasOne on any other model that
|
|
39
|
+
* points at a model backed by this table
|
|
40
|
+
* - no association anywhere in the app may name an ignored column as its
|
|
41
|
+
* primaryKeyOverride — the column the association's foreign key points
|
|
42
|
+
* at, which lives on the associated model's table for a BelongsTo and on
|
|
43
|
+
* the declaring model's own table for a HasMany/HasOne
|
|
44
|
+
*
|
|
45
|
+
* @param models - every model backed by the table
|
|
46
|
+
* @param tableName - the table whose ignored columns are being resolved
|
|
47
|
+
* @param allModels - every model in the app (on the table's connection);
|
|
48
|
+
* required because associations declared on other models can name foreign
|
|
49
|
+
* keys on this table
|
|
50
|
+
* @returns the set of column names to omit from the table's generated types
|
|
51
|
+
*/
|
|
52
|
+
export default function resolveIgnoredColumns(models, tableName, allModels) {
|
|
53
|
+
models.forEach(modelClass => {
|
|
54
|
+
const ignoredColumns = modelClass.prototype.ignoredColumns;
|
|
55
|
+
ignoredColumns.forEach(columnName => {
|
|
56
|
+
if (camelize(columnName) !== columnName)
|
|
57
|
+
throw new IgnoredColumnMustBeCamelCase(modelClass, columnName);
|
|
58
|
+
});
|
|
59
|
+
if (ignoredColumns.includes(modelClass.primaryKey))
|
|
60
|
+
throw new CannotIgnorePrimaryKey(modelClass);
|
|
61
|
+
if (ignoredColumns.includes('type') && (modelClass['isSTIBase'] || modelClass['isSTIChild']))
|
|
62
|
+
throw new CannotIgnoreStiTypeColumn(modelClass);
|
|
63
|
+
modelClass['sortableFields'].forEach(sortableFieldConfig => {
|
|
64
|
+
if (ignoredColumns.includes(sortableFieldConfig.positionField))
|
|
65
|
+
throw new CannotIgnoreSortablePositionColumn(modelClass, sortableFieldConfig.positionField);
|
|
66
|
+
// a Sortable scope entry names either a column or a BelongsTo
|
|
67
|
+
// association on the model (getColumnForSortableScope resolves columns
|
|
68
|
+
// first, then falls back to association metadata), so an ignored scope
|
|
69
|
+
// entry only breaks @Sortable when no association answers to the name
|
|
70
|
+
scopeArray(sortableFieldConfig.scope).forEach(scopeEntry => {
|
|
71
|
+
if (ignoredColumns.includes(scopeEntry) &&
|
|
72
|
+
modelClass['associationMetadataMap']()[scopeEntry] === undefined)
|
|
73
|
+
throw new CannotIgnoreSortableScopeColumn(modelClass, scopeEntry, sortableFieldConfig.positionField);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
// the Encrypted decorator records each backing column it manages in
|
|
77
|
+
// explicitUnsafeParamColumns (which nothing else populates)
|
|
78
|
+
modelClass['explicitUnsafeParamColumns'].forEach(columnName => {
|
|
79
|
+
if (ignoredColumns.includes(columnName))
|
|
80
|
+
throw new CannotIgnoreEncryptedColumn(modelClass, columnName);
|
|
81
|
+
});
|
|
82
|
+
if (modelClass['softDelete']) {
|
|
83
|
+
const deletedAtField = modelClass.prototype['_deletedAtField'];
|
|
84
|
+
if (ignoredColumns.includes(deletedAtField))
|
|
85
|
+
throw new CannotIgnoreSoftDeleteColumn(modelClass, deletedAtField);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
const distinctDeclarations = uniq(models.map(modelClass => JSON.stringify(uniq([...modelClass.prototype.ignoredColumns]).sort())));
|
|
89
|
+
if (distinctDeclarations.length > 1)
|
|
90
|
+
throw new ConflictingIgnoredColumns(tableName, models);
|
|
91
|
+
const ignoredColumns = new Set(models.flatMap(modelClass => [...modelClass.prototype.ignoredColumns]));
|
|
92
|
+
if (ignoredColumns.size)
|
|
93
|
+
guardAssociationColumns(ignoredColumns, tableName, allModels);
|
|
94
|
+
return ignoredColumns;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* @internal
|
|
98
|
+
*
|
|
99
|
+
* fails the sync when an ignored column of the table is the foreign key (or
|
|
100
|
+
* polymorphic type field) that any association in the app reads and writes
|
|
101
|
+
* on this table, or the primaryKeyOverride column that any association's
|
|
102
|
+
* foreign key points at on this table. A BelongsTo association names a
|
|
103
|
+
* foreign key on the declaring model's own table and a primaryKeyOverride
|
|
104
|
+
* on the associated model's table; a HasMany/HasOne association names a
|
|
105
|
+
* foreign key on the associated model's table and a primaryKeyOverride on
|
|
106
|
+
* the declaring model's own table — so the load-bearing association may be
|
|
107
|
+
* declared on any model in the app, not just the models backed by this
|
|
108
|
+
* table.
|
|
109
|
+
*/
|
|
110
|
+
function guardAssociationColumns(ignoredColumns, tableName, allModels) {
|
|
111
|
+
for (const modelClass of allModels) {
|
|
112
|
+
for (const associationName of modelClass.associationNames) {
|
|
113
|
+
const associationMetaData = modelClass['associationMetadataMap']()[associationName];
|
|
114
|
+
if (associationMetaData === undefined)
|
|
115
|
+
continue;
|
|
116
|
+
// a through association names no foreign key of its own; the source
|
|
117
|
+
// association it travels through is guarded directly
|
|
118
|
+
if (associationMetaData.through)
|
|
119
|
+
continue;
|
|
120
|
+
// unlike foreignKey(), primaryKeyOverride is a plain string on the
|
|
121
|
+
// association statement, so it can be guarded without consulting the
|
|
122
|
+
// generated schema — before the foreignKey-specific short-circuits below
|
|
123
|
+
const primaryKeyOverride = associationMetaData.primaryKeyOverride;
|
|
124
|
+
if (primaryKeyOverride &&
|
|
125
|
+
ignoredColumns.has(primaryKeyOverride) &&
|
|
126
|
+
primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName))
|
|
127
|
+
throw new CannotIgnoreAssociationColumn(tableName, primaryKeyOverride, modelClass, associationMetaData, 'primary key override');
|
|
128
|
+
if (!foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName))
|
|
129
|
+
continue;
|
|
130
|
+
// NOTE
|
|
131
|
+
// this try-catch mirrors getAssociationData in ASTConnectionBuilder:
|
|
132
|
+
// computing foreignKey() introspects columns via the generated schema
|
|
133
|
+
// file, so it may throw — on the first sync pass because the schema
|
|
134
|
+
// file has not been regenerated yet, and on the second pass because
|
|
135
|
+
// the regenerated schema omits the ignored column. So in orderings
|
|
136
|
+
// where foreignKey() throws on the first pass, it throws on the second
|
|
137
|
+
// pass too, and these guards never see the association's foreign key;
|
|
138
|
+
// the InvalidComputedForeignKey / ExplicitForeignKeyRequired errors
|
|
139
|
+
// swallowed here resurface at runtime as the backstop.
|
|
140
|
+
let foreignKey = null;
|
|
141
|
+
let foreignKeyTypeColumn = null;
|
|
142
|
+
try {
|
|
143
|
+
foreignKey = associationMetaData.foreignKey();
|
|
144
|
+
foreignKeyTypeColumn = associationMetaData.polymorphic
|
|
145
|
+
? associationMetaData.foreignKeyTypeField()
|
|
146
|
+
: null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (ignoredColumns.has(foreignKey))
|
|
152
|
+
throw new CannotIgnoreAssociationColumn(tableName, foreignKey, modelClass, associationMetaData, 'foreign key');
|
|
153
|
+
if (foreignKeyTypeColumn && ignoredColumns.has(foreignKeyTypeColumn))
|
|
154
|
+
throw new CannotIgnoreAssociationColumn(tableName, foreignKeyTypeColumn, modelClass, associationMetaData, 'polymorphic type field');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @internal
|
|
160
|
+
*
|
|
161
|
+
* returns whether the given association's foreign key physically lives on
|
|
162
|
+
* the given table: on the declaring model's own table for a BelongsTo, and
|
|
163
|
+
* on the associated model's table for a HasMany/HasOne
|
|
164
|
+
*/
|
|
165
|
+
function foreignKeyTableForAssociationMatches(associationMetaData, modelClass, tableName) {
|
|
166
|
+
if (associationMetaData.type === 'BelongsTo')
|
|
167
|
+
return modelClass.table === tableName;
|
|
168
|
+
const dreamClassOrClasses = associationMetaData.modelCB();
|
|
169
|
+
// a missing associated class raises FailedToIdentifyAssociation while the
|
|
170
|
+
// builder gathers association data; it is not this guard's concern
|
|
171
|
+
if (!dreamClassOrClasses)
|
|
172
|
+
return false;
|
|
173
|
+
return Array.isArray(dreamClassOrClasses)
|
|
174
|
+
? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
|
|
175
|
+
: dreamClassOrClasses.table === tableName;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* @internal
|
|
179
|
+
*
|
|
180
|
+
* returns whether the given association's primaryKeyOverride column
|
|
181
|
+
* physically lives on the given table — the opposite side from the foreign
|
|
182
|
+
* key (see associationPrimaryKeyAccessors in
|
|
183
|
+
* decorators/field/association/shared.ts): on the associated model's table
|
|
184
|
+
* for a BelongsTo, and on the declaring model's own table for a
|
|
185
|
+
* HasMany/HasOne
|
|
186
|
+
*/
|
|
187
|
+
function primaryKeyOverrideTableForAssociationMatches(associationMetaData, modelClass, tableName) {
|
|
188
|
+
if (associationMetaData.type !== 'BelongsTo')
|
|
189
|
+
return modelClass.table === tableName;
|
|
190
|
+
const dreamClassOrClasses = associationMetaData.modelCB();
|
|
191
|
+
// a missing associated class raises FailedToIdentifyAssociation while the
|
|
192
|
+
// builder gathers association data; it is not this guard's concern
|
|
193
|
+
if (!dreamClassOrClasses)
|
|
194
|
+
return false;
|
|
195
|
+
return Array.isArray(dreamClassOrClasses)
|
|
196
|
+
? dreamClassOrClasses.some(dreamClass => dreamClass.table === tableName)
|
|
197
|
+
: dreamClassOrClasses.table === tableName;
|
|
198
|
+
}
|
package/dist/esm/src/Dream.js
CHANGED
|
@@ -1461,6 +1461,12 @@ export default class Dream {
|
|
|
1461
1461
|
* 3. each nested association will result in an additional record which duplicates data from the outer record. E.g., given `.leftJoinPreload('a', 'b', 'c')`, if each `a` has 10 `b` and each `b` has 10 `c`, then for one `a`, 100 records will be returned, each of which has all of the columns of `a`. `.preload('a', 'b', 'c')` would perform three separate SQL queries, but the data for a single `a` would only be returned once.
|
|
1462
1462
|
* 4. the individual query becomes more complex the more associations are included
|
|
1463
1463
|
* 5. associations loading associations loading associations could result in exponential amounts of data; in those cases, `.preload(...).findEach(...)` avoids instantiating massive amounts of data at once
|
|
1464
|
+
* 6. leftJoinPreload must enumerate every compiled column of every joined
|
|
1465
|
+
* model, so unlike base-model reads, `preload`/`load`, and saves, it does
|
|
1466
|
+
* not tolerate schema/image skew from an unplanned column drop during a
|
|
1467
|
+
* rolling deploy; see {@link Query.leftJoinPreload} and the
|
|
1468
|
+
* `ignoredColumns` getter for the two-deploy process that makes a
|
|
1469
|
+
* planned drop safe.
|
|
1464
1470
|
*
|
|
1465
1471
|
* ```ts
|
|
1466
1472
|
* const user = await User.leftJoinPreload('posts', 'comments', { visibilty: 'public' }, 'replies').first()
|
|
@@ -2340,6 +2346,83 @@ export default class Dream {
|
|
|
2340
2346
|
get table() {
|
|
2341
2347
|
throw new DreamMissingRequiredOverride(this.constructor, 'table');
|
|
2342
2348
|
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Columns that Dream should behave as though they do not exist.
|
|
2351
|
+
*
|
|
2352
|
+
* Declaring a column ignored removes it from the generated types the next
|
|
2353
|
+
* time `sync` runs: it is omitted from both the db types file (the Kysely
|
|
2354
|
+
* `DB` interface) and the dream schema file, so it disappears from
|
|
2355
|
+
* `columns()` and from every place that flows from `columns()` — select
|
|
2356
|
+
* lists built for `preload`/`load` and `leftJoinPreload`, save hydration,
|
|
2357
|
+
* attribute definition, and param safety. References to the column in
|
|
2358
|
+
* application code become type errors, which is the point: they must be
|
|
2359
|
+
* removed before the column can be dropped.
|
|
2360
|
+
*
|
|
2361
|
+
* This enables safely dropping a column under rolling deploys — the same
|
|
2362
|
+
* problem Rails solves with `ignored_columns`. The safety requirement is
|
|
2363
|
+
* that no image that can run against the post-drop schema names the
|
|
2364
|
+
* column in any SQL it generates. To satisfy it: remove all application
|
|
2365
|
+
* code that uses the column, declare it here, and run `sync`; then let
|
|
2366
|
+
* the drop migration run only once no image lacking the declaration can
|
|
2367
|
+
* run against the database — whether the migration ships in a later
|
|
2368
|
+
* deploy of its own, or together with the declaration in a pipeline that
|
|
2369
|
+
* runs migrations only after the new images have rolled out. Once the
|
|
2370
|
+
* column is dropped, remove this declaration and resync.
|
|
2371
|
+
*
|
|
2372
|
+
* Precondition: as soon as an image built with this declaration runs,
|
|
2373
|
+
* the column is never placed in INSERT column lists, so before that
|
|
2374
|
+
* image can run, the column must be nullable or carry a database
|
|
2375
|
+
* default — a live `NOT NULL` column without a default fails every
|
|
2376
|
+
* create against the table.
|
|
2377
|
+
*
|
|
2378
|
+
* Dropping the column while an image that names it can still run leaves
|
|
2379
|
+
* a window during which those containers (including a rolled-back image)
|
|
2380
|
+
* fail with `42703 column does not exist` — `leftJoinPreload` in
|
|
2381
|
+
* particular has no runtime tolerance for this, since it must enumerate
|
|
2382
|
+
* aliased columns.
|
|
2383
|
+
*
|
|
2384
|
+
* This is a mechanism for the drop window, not for permanently hiding
|
|
2385
|
+
* wide columns: the ignored column is still transferred from the
|
|
2386
|
+
* database on every `RETURNING *` / `select *` until it is actually
|
|
2387
|
+
* dropped.
|
|
2388
|
+
*
|
|
2389
|
+
* The declaration is read only while `sync` generates the types files; it
|
|
2390
|
+
* has no runtime behavior of its own. A declared-but-not-synced model is
|
|
2391
|
+
* therefore not yet protected — CI should verify that `sync` produces no
|
|
2392
|
+
* diff. `sync` will fail loudly if a declared name is not camelCase
|
|
2393
|
+
* (generated column names are camelized, so any other shape could never
|
|
2394
|
+
* match and would be silently inert), if models sharing a table declare
|
|
2395
|
+
* different ignored columns, or if a model attempts to ignore a column
|
|
2396
|
+
* the framework itself reads and writes by name: its primary key, an STI
|
|
2397
|
+
* model's `type` column, any association's foreign key, polymorphic type
|
|
2398
|
+
* field, or `primaryKeyOverride` column, an `@Sortable` position field or
|
|
2399
|
+
* plain-column `@Sortable` scope, an `@Encrypted` backing column, or a
|
|
2400
|
+
* SoftDelete model's `deletedAt` column.
|
|
2401
|
+
*
|
|
2402
|
+
* Because an ignored column vanishes from `columns()`, runtime access via
|
|
2403
|
+
* type escape hatches behaves exactly like any unknown attribute: reads
|
|
2404
|
+
* return `undefined`, and writes assign a plain instance property that is
|
|
2405
|
+
* never persisted.
|
|
2406
|
+
*
|
|
2407
|
+
* ```ts
|
|
2408
|
+
* class User extends ApplicationModel {
|
|
2409
|
+
* public override get ignoredColumns() {
|
|
2410
|
+
* return ['legacyEmail'] as const
|
|
2411
|
+
* }
|
|
2412
|
+
* }
|
|
2413
|
+
* ```
|
|
2414
|
+
*
|
|
2415
|
+
* NOTE: this getter is intentionally typed as `readonly string[]` rather
|
|
2416
|
+
* than as a union of known column names: during the deploy that declares
|
|
2417
|
+
* a column ignored, the regenerated types no longer contain the column,
|
|
2418
|
+
* so a column-name-derived type would reject the very declaration that
|
|
2419
|
+
* removed it.
|
|
2420
|
+
*
|
|
2421
|
+
* @returns The list of column names this model should ignore
|
|
2422
|
+
*/
|
|
2423
|
+
get ignoredColumns() {
|
|
2424
|
+
return [];
|
|
2425
|
+
}
|
|
2343
2426
|
/**
|
|
2344
2427
|
* @internal
|
|
2345
2428
|
*
|
|
@@ -45,6 +45,8 @@ ${INDENT} - timetz
|
|
|
45
45
|
${INDENT} - timetz[]
|
|
46
46
|
${INDENT} - integer
|
|
47
47
|
${INDENT} - integer[]
|
|
48
|
+
${INDENT} - boolean
|
|
49
|
+
${INDENT} - boolean[]
|
|
48
50
|
${INDENT}
|
|
49
51
|
${INDENT} - decimal:
|
|
50
52
|
${INDENT} - decimal[]:
|
|
@@ -153,16 +155,17 @@ ${INDENT} # Add columns to an existing table (suffix with -to-<table_name> for
|
|
|
153
155
|
${INDENT} pnpm psy g:migration add-timezone-to-users timezone:string
|
|
154
156
|
${INDENT} pnpm psy g:migration add-bio-to-users bio:text:optional avatar_url:string:optional
|
|
155
157
|
${INDENT}
|
|
156
|
-
${INDENT} # Remove columns (suffix with -from-<table_name
|
|
157
|
-
${INDENT}
|
|
158
|
+
${INDENT} # Remove columns (suffix with -from-<table_name>; columns are described with
|
|
159
|
+
${INDENT} # their types, same shorthand as -to-, so the rollback (down) can recreate them)
|
|
160
|
+
${INDENT} pnpm psy g:migration remove-legacy-fields-from-posts legacy_status:string
|
|
158
161
|
${INDENT}
|
|
159
162
|
${INDENT} # General schema change (no table suffix — generates empty up/down methods)
|
|
160
163
|
${INDENT} pnpm psy g:migration create-unique-index-on-invitations`)
|
|
161
164
|
.argument('<migrationName>', `Kebab-case name describing the change. End with -to-<table_name> or -from-<table_name> to auto-generate an alterTable scaffold for that table.
|
|
162
165
|
${INDENT}
|
|
163
166
|
${INDENT}Examples:
|
|
164
|
-
${INDENT} add-phone-to-users
|
|
165
|
-
${INDENT} remove-status-from-posts
|
|
167
|
+
${INDENT} add-phone-to-users phone:encrypted # scaffolds alterTable('users', ...)
|
|
168
|
+
${INDENT} remove-status-from-posts status:string # scaffolds alterTable('posts', ...)
|
|
166
169
|
${INDENT} create-join-table-host-places # empty migration (no table suffix match)`)
|
|
167
170
|
.option('--connection-name <connectionName>', 'the database connection to use for this migration. Only needed for multi-database setups; defaults to "default"')
|
|
168
171
|
.argument('[columnsWithTypes...]', columnsWithTypesDescriptionForMigration)
|