@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
|
@@ -442,6 +442,17 @@ export default class Query {
|
|
|
442
442
|
* 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.
|
|
443
443
|
* 4. the individual query becomes more complex the more associations are included
|
|
444
444
|
* 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
|
|
445
|
+
* 6. unlike base-model reads, `preload`/`load`, and saves — which tolerate
|
|
446
|
+
* schema/image skew (e.g. a rolling deploy dropping a column while
|
|
447
|
+
* containers compiled against the previous schema are still draining) —
|
|
448
|
+
* leftJoinPreload must enumerate every compiled column of every joined
|
|
449
|
+
* model under per-alias names (per-alias `*` is not expressible in a
|
|
450
|
+
* single flat row), so an **unplanned** column drop breaks
|
|
451
|
+
* leftJoinPreload queries for the duration of the rollout window. A
|
|
452
|
+
* **planned** drop is safe when performed via the two-deploy process
|
|
453
|
+
* documented on the `ignoredColumns` getter of Dream: declaring the
|
|
454
|
+
* column ignored removes it from the generated schema, so leftJoinPreload
|
|
455
|
+
* stops naming it a full deploy before the column is actually dropped.
|
|
445
456
|
*
|
|
446
457
|
*
|
|
447
458
|
* ```ts
|
|
@@ -734,6 +734,27 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
734
734
|
* is provided as a second argument, it will use that transaction
|
|
735
735
|
* to encapsulate the persisting of the dream, as well as any
|
|
736
736
|
* subsequent model hooks that are fired.
|
|
737
|
+
*
|
|
738
|
+
* `RETURNING *` (rather than enumerating the compiled column list) keeps
|
|
739
|
+
* writes working under schema/image skew: during a rolling deploy, a
|
|
740
|
+
* container built before a drop-column migration would otherwise name the
|
|
741
|
+
* dropped column in `RETURNING` and fail with `42703 column does not
|
|
742
|
+
* exist` on every write, even writes that never touch that column. The
|
|
743
|
+
* `SET`/`VALUES` half still names only dirty attributes, so a write that
|
|
744
|
+
* actually sets a dropped column still fails loudly. The returned row is
|
|
745
|
+
* filtered to the compiled column list before hydration (see
|
|
746
|
+
* internal/saveDream.ts), so a column the image doesn't know about never
|
|
747
|
+
* reaches `setAttributes`.
|
|
748
|
+
*
|
|
749
|
+
* KNOWN CONSTRAINT: star-selects are only safe because nothing in this
|
|
750
|
+
* stack uses named prepared statements — node-postgres prepares a
|
|
751
|
+
* statement only when given an explicit `name`, and Kysely never names
|
|
752
|
+
* them, so every query is re-planned. With named prepared statements, a
|
|
753
|
+
* concurrent `ADD COLUMN` changes a cached plan's result shape and
|
|
754
|
+
* Postgres raises `cached plan must not change result type` (the reason
|
|
755
|
+
* Rails added `enumerate_columns_in_select_statements`). If a future
|
|
756
|
+
* driver or pooling layer enables named prepared statements, revisit
|
|
757
|
+
* every `RETURNING *` / `select *` in this driver.
|
|
737
758
|
*/
|
|
738
759
|
static async saveDream(dream, txn = null) {
|
|
739
760
|
const connectionName = dream.connectionName || 'default';
|
|
@@ -745,13 +766,13 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
745
766
|
.updateTable(dream.table)
|
|
746
767
|
.set(sqlifiedAttributes)
|
|
747
768
|
.where(namespaceColumn(dream['_primaryKey'], dream.table), '=', dream.primaryKeyValue());
|
|
748
|
-
return await executeDatabaseQuery(query.
|
|
769
|
+
return await executeDatabaseQuery(query.returningAll(), 'executeTakeFirstOrThrow');
|
|
749
770
|
}
|
|
750
771
|
else {
|
|
751
772
|
const query = db
|
|
752
773
|
.insertInto(dream.table)
|
|
753
774
|
.values(sqlifiedAttributes)
|
|
754
|
-
.
|
|
775
|
+
.returningAll();
|
|
755
776
|
return await executeDatabaseQuery(query, 'executeTakeFirstOrThrow');
|
|
756
777
|
}
|
|
757
778
|
}
|
|
@@ -2401,15 +2422,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2401
2422
|
const preloadedPolymorphicBelongsTos = await this.preloadPolymorphicBelongsTo(association, dreamClassToHydrate, dreams);
|
|
2402
2423
|
return preloadedPolymorphicBelongsTos;
|
|
2403
2424
|
}
|
|
2404
|
-
const dreamClassToHydrateColumns =
|
|
2405
|
-
|
|
2406
|
-
|
|
2425
|
+
const dreamClassToHydrateColumns = dreamClassToHydrate.columns();
|
|
2426
|
+
// The base-model primary key rides along under a short alias so each
|
|
2427
|
+
// associated row can be matched back to the dream it belongs to. The
|
|
2428
|
+
// alias is lowercase without underscores so Kysely's CamelCasePlugin
|
|
2429
|
+
// passes it through untouched (the same strategy as `pluck0` /
|
|
2430
|
+
// `groupvalue`), and is extended until it cannot collide with a real
|
|
2431
|
+
// column of the associated table. That collision loop can only consult
|
|
2432
|
+
// the compiled column set, so under add-column skew a live column
|
|
2433
|
+
// literally named like the alias would produce two identically-named
|
|
2434
|
+
// result fields. The aliased base PK is deliberately selected last,
|
|
2435
|
+
// and node-postgres resolves duplicate field names last-wins, so the
|
|
2436
|
+
// base PK still wins even in that pathological case — and the
|
|
2437
|
+
// hydration filter strips the colliding key from the instance's
|
|
2438
|
+
// attributes.
|
|
2439
|
+
let basePrimaryKeyAlias = 'preloadbasepk';
|
|
2440
|
+
while (dreamClassToHydrateColumns.has(basePrimaryKeyAlias))
|
|
2441
|
+
basePrimaryKeyAlias += '0';
|
|
2407
2442
|
const baseClass = dreamClass['stiBaseClassOrOwnClass']['getAssociationMetadata'](associationName)
|
|
2408
2443
|
? dreamClass['stiBaseClassOrOwnClass']
|
|
2409
2444
|
: dreamClass;
|
|
2410
2445
|
const associationDataScope = this.dreamClassQueryWithScopeBypasses(baseClass, {
|
|
2411
2446
|
// In order to stay DRY, preloading leverages the association logic built into
|
|
2412
|
-
// `joins
|
|
2447
|
+
// `joins`. However, baseClass may have
|
|
2413
2448
|
// default scopes that would preclude finding that instance. We remove all
|
|
2414
2449
|
// default scopes on baseClass, but not subsequent associations, so that the
|
|
2415
2450
|
// single query will be able to find each row corresponding to a Dream in `dreams`,
|
|
@@ -2418,16 +2453,29 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2418
2453
|
}).where({
|
|
2419
2454
|
[dreamClass.primaryKey]: dreams.map(obj => obj.primaryKeyValue()),
|
|
2420
2455
|
});
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2456
|
+
const joinedQuery = associationDataScope['_connection'](this.connectionOverride).innerJoin(associationName, (onStatement || {}));
|
|
2457
|
+
// Select the association's row wholesale (`"alias".*`) rather than
|
|
2458
|
+
// enumerating the compiled column list. Enumerating made every preload
|
|
2459
|
+
// assert the compiled schema against the live database: under
|
|
2460
|
+
// schema/image skew (rolling deploy or rollback around a drop-column
|
|
2461
|
+
// migration), naming a dropped column fails the whole preload with
|
|
2462
|
+
// `42703 column does not exist`. The raw row is passed straight to
|
|
2463
|
+
// hydration, where sqlResultToDreamInstance — the single filtering
|
|
2464
|
+
// point — intersects it with the compiled column list (also stripping
|
|
2465
|
+
// the base-PK alias key), so a column the image doesn't know about
|
|
2466
|
+
// (the add-column direction of skew) never reaches the instance. See
|
|
2467
|
+
// the prepared-statements note on `saveDream` before changing this
|
|
2468
|
+
// back to an enumerated select.
|
|
2469
|
+
const kyselyQuery = new this.constructor(joinedQuery)
|
|
2470
|
+
.buildSelect({ bypassSelectAll: true })
|
|
2471
|
+
.selectAll(alias)
|
|
2472
|
+
.select(`${this.namespaceColumn(dreamClass.primaryKey, dreamClass.table)} as ${basePrimaryKeyAlias}`);
|
|
2473
|
+
const rows = await executeDatabaseQuery(kyselyQuery, 'execute');
|
|
2474
|
+
const preloadedDreamsAndWhatTheyPointTo = rows.map((row) => {
|
|
2475
|
+
const hydratedDream = this.dbResultToDreamInstance(row, dreamClassToHydrate);
|
|
2428
2476
|
return {
|
|
2429
2477
|
dream: hydratedDream,
|
|
2430
|
-
pointsToPrimaryKey:
|
|
2478
|
+
pointsToPrimaryKey: row[basePrimaryKeyAlias],
|
|
2431
2479
|
};
|
|
2432
2480
|
});
|
|
2433
2481
|
this.hydrateAssociation(dreams, association, preloadedDreamsAndWhatTheyPointTo);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import protectAgainstPollutingAssignment from '../../helpers/protectAgainstPollutingAssignment.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Returns an object containing only the keys of `row` that are columns
|
|
6
|
+
* the compiled schema knows about (per the provided column set).
|
|
7
|
+
*
|
|
8
|
+
* Under schema/image skew (e.g. a rolling deploy in which a migration adds
|
|
9
|
+
* a column while containers built against the previous schema are still
|
|
10
|
+
* draining, or application code is rolled back after an add-column
|
|
11
|
+
* migration), a `RETURNING *` / `select *` row can include columns this
|
|
12
|
+
* build has never heard of. Passing such keys to `setAttributes` would
|
|
13
|
+
* assign them as plain properties — invoking a same-named user-defined
|
|
14
|
+
* setter, or throwing on a getter-only property — so they must be dropped
|
|
15
|
+
* before hydration.
|
|
16
|
+
*
|
|
17
|
+
* Keys are intersected rather than enumerated from the column set so that
|
|
18
|
+
* a column missing from the row (the dropped-column direction of skew)
|
|
19
|
+
* simply does not appear, rather than appearing with an `undefined` value.
|
|
20
|
+
*
|
|
21
|
+
* Identity fast path: when every key of `row` is a known column (the
|
|
22
|
+
* no-skew steady state), `row` itself is returned unchanged, without
|
|
23
|
+
* allocating a copy; a filtered copy is built only when at least one
|
|
24
|
+
* unknown key is present.
|
|
25
|
+
*
|
|
26
|
+
* @param row - a raw database row
|
|
27
|
+
* @param columns - the compiled column set for the Dream class being hydrated
|
|
28
|
+
* @returns `row` itself when every key is a known column; otherwise a new
|
|
29
|
+
* object containing only the known-column entries of `row`
|
|
30
|
+
*/
|
|
31
|
+
export default function filterRowToKnownColumns(row, columns) {
|
|
32
|
+
const keys = Object.keys(row);
|
|
33
|
+
if (keys.every(key => columns.has(key)))
|
|
34
|
+
return row;
|
|
35
|
+
const filtered = {};
|
|
36
|
+
keys.forEach(key => {
|
|
37
|
+
if (columns.has(key))
|
|
38
|
+
filtered[protectAgainstPollutingAssignment(key)] = row[key];
|
|
39
|
+
});
|
|
40
|
+
return filtered;
|
|
41
|
+
}
|
|
@@ -2,6 +2,7 @@ import ValidationError from '../../errors/ValidationError.js';
|
|
|
2
2
|
import sqlAttributes from '../../helpers/sqlAttributes.js';
|
|
3
3
|
import { DateTime } from '../../utils/datetime/DateTime.js';
|
|
4
4
|
import Query from '../Query.js';
|
|
5
|
+
import filterRowToKnownColumns from './filterRowToKnownColumns.js';
|
|
5
6
|
import runHooksFor from './runHooksFor.js';
|
|
6
7
|
export default async function saveDream(dream, txn = null, { skipHooks = false } = {}) {
|
|
7
8
|
const alreadyPersisted = dream.isPersisted;
|
|
@@ -30,7 +31,11 @@ export default async function saveDream(dream, txn = null, { skipHooks = false }
|
|
|
30
31
|
if (!alreadyPersisted || hasUnsavedData) {
|
|
31
32
|
const data = await Query.dbDriverClass(dream.connectionName || 'default').saveDream(dream, txn);
|
|
32
33
|
dream['isPersisted'] = true;
|
|
33
|
-
|
|
34
|
+
// the row returned by the driver comes from `RETURNING *` (or a
|
|
35
|
+
// `select *` reload on drivers without RETURNING support), so under
|
|
36
|
+
// schema/image skew it can contain columns this build's compiled
|
|
37
|
+
// schema doesn't know about; those must never reach setAttributes
|
|
38
|
+
dream.setAttributes(filterRowToKnownColumns(data, dream.columns()));
|
|
34
39
|
}
|
|
35
40
|
// set frozen attributes to what has already been saved
|
|
36
41
|
dream['freezeAttributes']();
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import STIChildMissing from '../../errors/sti/STIChildMissing.js';
|
|
2
|
+
import filterRowToKnownColumns from './filterRowToKnownColumns.js';
|
|
2
3
|
export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
|
|
4
|
+
// base-model reads select `*`, so under schema/image skew (or with a
|
|
5
|
+
// column declared in ignoredColumns but not yet dropped) the row can
|
|
6
|
+
// contain columns the compiled schema doesn't know about. Passing such
|
|
7
|
+
// keys into the constructor would assign them as plain properties —
|
|
8
|
+
// invoking a same-named user-defined setter, or throwing on a
|
|
9
|
+
// getter-only property — so they are dropped before hydration.
|
|
3
10
|
if (dreamClass['isSTIBase']) {
|
|
4
11
|
const extendingDreamClass = findExtendingDreamClass(dreamClass, sqlResult.type);
|
|
5
12
|
if (!extendingDreamClass)
|
|
6
13
|
throw new STIChildMissing(dreamClass, sqlResult.type, sqlResult[dreamClass.primaryKey]);
|
|
7
|
-
const dreamModel = new extendingDreamClass(sqlResult, {
|
|
14
|
+
const dreamModel = new extendingDreamClass(filterRowToKnownColumns(sqlResult, extendingDreamClass.columns()), {
|
|
8
15
|
bypassUserDefinedSetters: true,
|
|
9
16
|
isPersisted: true,
|
|
10
17
|
_internalUseOnly: true,
|
|
@@ -13,7 +20,7 @@ export default function sqlResultToDreamInstance(dreamClass, sqlResult) {
|
|
|
13
20
|
return dreamModel;
|
|
14
21
|
}
|
|
15
22
|
else {
|
|
16
|
-
const dreamModel = new dreamClass(sqlResult, {
|
|
23
|
+
const dreamModel = new dreamClass(filterRowToKnownColumns(sqlResult, dreamClass.columns()), {
|
|
17
24
|
bypassUserDefinedSetters: true,
|
|
18
25
|
isPersisted: true,
|
|
19
26
|
_internalUseOnly: true,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class NoColumnsToAlterMigration extends Error {
|
|
2
|
+
table;
|
|
3
|
+
alterDirection;
|
|
4
|
+
constructor(table, alterDirection) {
|
|
5
|
+
super();
|
|
6
|
+
this.table = table;
|
|
7
|
+
this.alterDirection = alterDirection;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return `
|
|
11
|
+
no valid columns to ${this.alterDirection === 'remove' ? 'drop from' : 'add to'} table '${this.table}' in this alter migration.
|
|
12
|
+
|
|
13
|
+
Pass at least one column declaration with a resolvable type, e.g.:
|
|
14
|
+
name:string
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default class UnparseableMigrationColumn extends Error {
|
|
2
|
+
declaration;
|
|
3
|
+
constructor(declaration) {
|
|
4
|
+
super();
|
|
5
|
+
this.declaration = declaration;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
could not determine a column type for the following migration column declaration:
|
|
10
|
+
${this.declaration}
|
|
11
|
+
|
|
12
|
+
Make sure the column includes a type, e.g.:
|
|
13
|
+
${this.declaration.split(':')[0]}:string
|
|
14
|
+
`;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export default class CannotIgnoreAssociationColumn extends Error {
|
|
2
|
+
tableName;
|
|
3
|
+
columnName;
|
|
4
|
+
modelClass;
|
|
5
|
+
association;
|
|
6
|
+
columnRole;
|
|
7
|
+
constructor(tableName, columnName, modelClass, association, columnRole) {
|
|
8
|
+
super();
|
|
9
|
+
this.tableName = tableName;
|
|
10
|
+
this.columnName = columnName;
|
|
11
|
+
this.modelClass = modelClass;
|
|
12
|
+
this.association = association;
|
|
13
|
+
this.columnRole = columnRole;
|
|
14
|
+
}
|
|
15
|
+
get message() {
|
|
16
|
+
return `
|
|
17
|
+
The models backed by the "${this.tableName}" table declare
|
|
18
|
+
"${this.columnName}" in ignoredColumns, but "${this.columnName}" is the
|
|
19
|
+
${this.columnRole} of the ${this.association.type} association
|
|
20
|
+
"${this.association.as}" on ${this.modelClass.sanitizedName}.
|
|
21
|
+
|
|
22
|
+
Associations read and write their columns by name, so ignoring
|
|
23
|
+
"${this.columnName}" would silently break "${this.association.as}" at
|
|
24
|
+
runtime: ${this.consequence}. Remove the
|
|
25
|
+
"${this.association.as}" association from ${this.modelClass.sanitizedName}
|
|
26
|
+
(or point it at a different column) before ignoring "${this.columnName}".
|
|
27
|
+
`;
|
|
28
|
+
}
|
|
29
|
+
get consequence() {
|
|
30
|
+
if (this.columnRole === 'primary key override')
|
|
31
|
+
return `associating a record would read "${this.columnName}" from an
|
|
32
|
+
instance that no longer hydrates it, silently persisting an empty foreign
|
|
33
|
+
key, and association loads would join on a column missing from the
|
|
34
|
+
generated schema`;
|
|
35
|
+
return `writes to the column would stop persisting, and association loads
|
|
36
|
+
would reference a column missing from the generated schema`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default class CannotIgnoreEncryptedColumn extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
columnName;
|
|
4
|
+
constructor(modelClass, columnName) {
|
|
5
|
+
super();
|
|
6
|
+
this.modelClass = modelClass;
|
|
7
|
+
this.columnName = columnName;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return `
|
|
11
|
+
${this.modelClass.sanitizedName} declares "${this.columnName}" in
|
|
12
|
+
ignoredColumns, but "${this.columnName}" is the backing column of an
|
|
13
|
+
@Encrypted property on ${this.modelClass.sanitizedName}.
|
|
14
|
+
|
|
15
|
+
@Encrypted stores its ciphertext in this column, so it can never be
|
|
16
|
+
ignored. Remove the @Encrypted declaration (and its property) from
|
|
17
|
+
${this.modelClass.sanitizedName} before ignoring "${this.columnName}".
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default class CannotIgnorePrimaryKey extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
constructor(modelClass) {
|
|
4
|
+
super();
|
|
5
|
+
this.modelClass = modelClass;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
${this.modelClass.sanitizedName} declares its primary key ("${this.modelClass.primaryKey}")
|
|
10
|
+
in ignoredColumns.
|
|
11
|
+
|
|
12
|
+
The primary key is required to identify rows for hydration, updates, and
|
|
13
|
+
associations, so it can never be ignored. Remove
|
|
14
|
+
"${this.modelClass.primaryKey}" from the ignoredColumns getter on
|
|
15
|
+
${this.modelClass.sanitizedName}.
|
|
16
|
+
`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default class CannotIgnoreSoftDeleteColumn extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
columnName;
|
|
4
|
+
constructor(modelClass, columnName) {
|
|
5
|
+
super();
|
|
6
|
+
this.modelClass = modelClass;
|
|
7
|
+
this.columnName = columnName;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return `
|
|
11
|
+
${this.modelClass.sanitizedName} declares "${this.columnName}" in
|
|
12
|
+
ignoredColumns, but ${this.modelClass.sanitizedName} is a SoftDelete model
|
|
13
|
+
and "${this.columnName}" is its deletedAtField.
|
|
14
|
+
|
|
15
|
+
SoftDelete writes this column on destroy and filters on it in its default
|
|
16
|
+
scope, so it can never be ignored. Remove the @SoftDelete decorator from
|
|
17
|
+
${this.modelClass.sanitizedName} (or point deletedAtField at a different
|
|
18
|
+
column) before ignoring "${this.columnName}".
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export default class CannotIgnoreSortablePositionColumn extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
columnName;
|
|
4
|
+
constructor(modelClass, columnName) {
|
|
5
|
+
super();
|
|
6
|
+
this.modelClass = modelClass;
|
|
7
|
+
this.columnName = columnName;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
return `
|
|
11
|
+
${this.modelClass.sanitizedName} declares "${this.columnName}" in
|
|
12
|
+
ignoredColumns, but "${this.columnName}" is the position field of an
|
|
13
|
+
@Sortable declaration on ${this.modelClass.sanitizedName}.
|
|
14
|
+
|
|
15
|
+
@Sortable reads and rewrites this column to keep positions contiguous, so
|
|
16
|
+
it can never be ignored. Remove the @Sortable decorator (and its property)
|
|
17
|
+
from ${this.modelClass.sanitizedName} before ignoring "${this.columnName}".
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default class CannotIgnoreSortableScopeColumn extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
columnName;
|
|
4
|
+
positionField;
|
|
5
|
+
constructor(modelClass, columnName, positionField) {
|
|
6
|
+
super();
|
|
7
|
+
this.modelClass = modelClass;
|
|
8
|
+
this.columnName = columnName;
|
|
9
|
+
this.positionField = positionField;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
${this.modelClass.sanitizedName} declares "${this.columnName}" in
|
|
14
|
+
ignoredColumns, but "${this.columnName}" is a scope column of the @Sortable
|
|
15
|
+
declaration on "${this.positionField}" on ${this.modelClass.sanitizedName}.
|
|
16
|
+
|
|
17
|
+
@Sortable reads this column by name on every save and destroy to partition
|
|
18
|
+
position values, so it can never be ignored. Remove "${this.columnName}"
|
|
19
|
+
from the @Sortable scope on "${this.positionField}" (or remove the
|
|
20
|
+
@Sortable decorator and its property) on ${this.modelClass.sanitizedName}
|
|
21
|
+
before ignoring "${this.columnName}".
|
|
22
|
+
`;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default class CannotIgnoreStiTypeColumn extends Error {
|
|
2
|
+
modelClass;
|
|
3
|
+
constructor(modelClass) {
|
|
4
|
+
super();
|
|
5
|
+
this.modelClass = modelClass;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
${this.modelClass.sanitizedName} participates in single-table inheritance
|
|
10
|
+
and declares the STI "type" column in ignoredColumns.
|
|
11
|
+
|
|
12
|
+
The "type" column determines which model class each row hydrates into, so
|
|
13
|
+
an STI model can never ignore it. Remove "type" from the ignoredColumns
|
|
14
|
+
getter on ${this.modelClass.sanitizedName}.
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export default class ConflictingIgnoredColumns extends Error {
|
|
2
|
+
tableName;
|
|
3
|
+
modelClasses;
|
|
4
|
+
constructor(tableName, modelClasses) {
|
|
5
|
+
super();
|
|
6
|
+
this.tableName = tableName;
|
|
7
|
+
this.modelClasses = modelClasses;
|
|
8
|
+
}
|
|
9
|
+
get message() {
|
|
10
|
+
const declarations = this.modelClasses
|
|
11
|
+
.map(modelClass => ` ${modelClass.sanitizedName}: [${[...modelClass.prototype.ignoredColumns]
|
|
12
|
+
.sort()
|
|
13
|
+
.map(column => `'${column}'`)
|
|
14
|
+
.join(', ')}]`)
|
|
15
|
+
.join('\n');
|
|
16
|
+
return `
|
|
17
|
+
Models sharing the table "${this.tableName}" declare different ignoredColumns:
|
|
18
|
+
|
|
19
|
+
${declarations}
|
|
20
|
+
|
|
21
|
+
There is only one generated schema per table, so every model backed by a
|
|
22
|
+
table must agree on which of its columns are ignored. Align the
|
|
23
|
+
ignoredColumns getters (STI children inherit the base model's getter, so
|
|
24
|
+
the common fix is to declare ignored columns only on the base model).
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import camelize from '../../helpers/camelize.js';
|
|
2
|
+
export default class IgnoredColumnMustBeCamelCase extends Error {
|
|
3
|
+
modelClass;
|
|
4
|
+
columnName;
|
|
5
|
+
constructor(modelClass, columnName) {
|
|
6
|
+
super();
|
|
7
|
+
this.modelClass = modelClass;
|
|
8
|
+
this.columnName = columnName;
|
|
9
|
+
}
|
|
10
|
+
get message() {
|
|
11
|
+
return `
|
|
12
|
+
${this.modelClass.sanitizedName} declares "${this.columnName}" in
|
|
13
|
+
ignoredColumns, but ignored columns must be declared in camelCase.
|
|
14
|
+
|
|
15
|
+
Dream camelizes database column names when generating types, so a
|
|
16
|
+
declaration that is not camelCase can never match a generated column and
|
|
17
|
+
would be silently inert. Declare "${camelize(this.columnName)}" in the
|
|
18
|
+
ignoredColumns getter on ${this.modelClass.sanitizedName} instead.
|
|
19
|
+
`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -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
|
*
|