@rvoh/dream 2.20.0 → 2.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/Dream.js +3 -0
- package/dist/cjs/src/cli/index.js +7 -4
- package/dist/cjs/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/cjs/src/dream/Query.js +203 -4
- package/dist/cjs/src/dream/QueryDriver/Base.js +26 -0
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/cjs/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/cjs/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/cjs/src/dream/internal/destroyDream.js +6 -7
- package/dist/cjs/src/errors/InvalidBatchSize.js +19 -0
- package/dist/cjs/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/cjs/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/cjs/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/cjs/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -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/ops/index.js +19 -0
- package/dist/esm/src/Dream.js +3 -0
- package/dist/esm/src/cli/index.js +7 -4
- package/dist/esm/src/dream/DreamClassTransactionBuilder.js +3 -0
- package/dist/esm/src/dream/Query.js +203 -4
- package/dist/esm/src/dream/QueryDriver/Base.js +26 -0
- package/dist/esm/src/dream/QueryDriver/Kysely.js +46 -4
- package/dist/esm/src/dream/QueryDriver/Postgres.js +21 -0
- package/dist/esm/src/dream/internal/destroyAssociatedRecords.js +76 -4
- package/dist/esm/src/dream/internal/destroyDream.js +6 -7
- package/dist/esm/src/errors/InvalidBatchSize.js +19 -0
- package/dist/esm/src/errors/NoColumnsToAlterMigration.js +17 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithDistinct.js +14 -0
- package/dist/esm/src/errors/RowLockIncompatibleWithJoinLoad.js +14 -0
- package/dist/esm/src/errors/UnparseableMigrationColumn.js +16 -0
- package/dist/esm/src/errors/ops/ArrayEqualityRequiresArrayColumn.js +23 -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/ops/index.js +19 -0
- package/dist/types/src/Dream.d.ts +3 -0
- package/dist/types/src/cli/index.d.ts +1 -1
- package/dist/types/src/dream/DreamClassTransactionBuilder.d.ts +3 -0
- package/dist/types/src/dream/Query.d.ts +140 -3
- package/dist/types/src/dream/QueryDriver/Base.d.ts +21 -0
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -0
- package/dist/types/src/dream/QueryDriver/Postgres.d.ts +20 -0
- package/dist/types/src/dream/internal/destroyAssociatedRecords.d.ts +22 -3
- package/dist/types/src/errors/InvalidBatchSize.d.ts +5 -0
- package/dist/types/src/errors/NoColumnsToAlterMigration.d.ts +6 -0
- package/dist/types/src/errors/RowLockIncompatibleWithDistinct.d.ts +3 -0
- package/dist/types/src/errors/RowLockIncompatibleWithJoinLoad.d.ts +3 -0
- package/dist/types/src/errors/UnparseableMigrationColumn.d.ts +5 -0
- package/dist/types/src/errors/ops/ArrayEqualityRequiresArrayColumn.d.ts +8 -0
- package/dist/types/src/helpers/cli/generateMigrationContent.d.ts +11 -1
- package/dist/types/src/ops/index.d.ts +19 -0
- package/dist/types/src/types/associations/shared.d.ts +2 -2
- package/dist/types/src/types/dream.d.ts +8 -0
- package/dist/types/src/types/types/associations/shared.ts +84 -55
- package/dist/types/src/types/types/dream.ts +30 -0
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +55 -41
- package/docs/classes/db.PostgresQueryDriver.html +55 -42
- package/docs/classes/db.QueryDriverBase.html +54 -40
- 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 +127 -125
- 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 +161 -71
- 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/hierarchy.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 +38 -23
- 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
|
@@ -256,6 +256,32 @@ export default class QueryDriverBase {
|
|
|
256
256
|
options = {}) {
|
|
257
257
|
throw new Error('implement takeAll in child class');
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* @internal
|
|
261
|
+
*
|
|
262
|
+
* Adapter seam for exclusive row locking. Given the select statement about to
|
|
263
|
+
* be executed, return a statement that additionally acquires an exclusive
|
|
264
|
+
* lock on each row it returns, held until the enclosing transaction commits
|
|
265
|
+
* or rolls back (Postgres: `SELECT ... FOR UPDATE OF <table>`).
|
|
266
|
+
*
|
|
267
|
+
* This is only ever reached when a caller opts into a locked read (today,
|
|
268
|
+
* `Query#destroy({ lock: true })`), and only from inside a transaction. The
|
|
269
|
+
* base implementation throws so that an adapter which has not implemented row
|
|
270
|
+
* locking fails loud rather than silently performing an unlocked read and
|
|
271
|
+
* quietly dropping the compare-and-set guarantee the caller asked for.
|
|
272
|
+
*
|
|
273
|
+
* @param kyselyQuery - the select statement to lock
|
|
274
|
+
* @param tableAlias - the alias of the Query's base table; only rows of that
|
|
275
|
+
* table are locked, so a joined query does not lock unrelated rows and does
|
|
276
|
+
* not fail on the nullable side of an outer join
|
|
277
|
+
*/
|
|
278
|
+
applyRowLock(
|
|
279
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
280
|
+
kyselyQuery,
|
|
281
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
282
|
+
tableAlias) {
|
|
283
|
+
throw new Error('implement applyRowLock in child class');
|
|
284
|
+
}
|
|
259
285
|
/**
|
|
260
286
|
* Retrieves the max value of the specified column
|
|
261
287
|
* for this Query
|
|
@@ -29,6 +29,9 @@ import MissingThroughAssociationSource from '../../errors/associations/MissingTh
|
|
|
29
29
|
import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
|
|
30
30
|
import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
|
|
31
31
|
import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
|
|
32
|
+
import ArrayEqualityRequiresArrayColumn from '../../errors/ops/ArrayEqualityRequiresArrayColumn.js';
|
|
33
|
+
import RowLockIncompatibleWithDistinct from '../../errors/RowLockIncompatibleWithDistinct.js';
|
|
34
|
+
import RowLockIncompatibleWithJoinLoad from '../../errors/RowLockIncompatibleWithJoinLoad.js';
|
|
32
35
|
import CheckConstraintViolation from '../../errors/db/CheckConstraintViolation.js';
|
|
33
36
|
import ColumnOverflow from '../../errors/db/ColumnOverflow.js';
|
|
34
37
|
import DataTypeColumnTypeMismatch from '../../errors/db/DataTypeColumnTypeMismatch.js';
|
|
@@ -39,6 +42,7 @@ import ASTSchemaBuilder from '../../helpers/cli/ASTSchemaBuilder.js';
|
|
|
39
42
|
import generateMigration from '../../helpers/cli/generateMigration.js';
|
|
40
43
|
import compact from '../../helpers/compact.js';
|
|
41
44
|
import { normalizeDataForDb } from '../../helpers/db/normalizeDataForDb.js';
|
|
45
|
+
import isDatabaseArrayColumn from '../../helpers/db/types/isDatabaseArrayColumn.js';
|
|
42
46
|
import EnvInternal from '../../helpers/EnvInternal.js';
|
|
43
47
|
import groupBy from '../../helpers/groupBy.js';
|
|
44
48
|
import isEmpty from '../../helpers/isEmpty.js';
|
|
@@ -412,9 +416,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
412
416
|
* @returns an array of dreams
|
|
413
417
|
*/
|
|
414
418
|
async takeAll(options = {}) {
|
|
415
|
-
if (this.query['joinLoadActivated'])
|
|
419
|
+
if (this.query['joinLoadActivated']) {
|
|
420
|
+
// executeJoinLoad builds its own select and has nowhere to put a row
|
|
421
|
+
// lock, so a locked read here has to fail rather than quietly return
|
|
422
|
+
// unlocked rows and drop the guarantee the caller asked for
|
|
423
|
+
if (options.lock)
|
|
424
|
+
throw new RowLockIncompatibleWithJoinLoad();
|
|
416
425
|
return await this.executeJoinLoad(options);
|
|
417
|
-
|
|
426
|
+
}
|
|
427
|
+
// `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database, so
|
|
428
|
+
// fail loudly here instead of emitting SQL that cannot run
|
|
429
|
+
if (options.lock && this.query['distinctColumn'])
|
|
430
|
+
throw new RowLockIncompatibleWithDistinct();
|
|
431
|
+
let kyselyQuery = this.buildSelect(options);
|
|
432
|
+
// row locking is adapter-specific, so it goes through the query driver seam
|
|
433
|
+
if (options.lock)
|
|
434
|
+
kyselyQuery = this.applyRowLock(kyselyQuery, this.query['baseSqlAlias']);
|
|
418
435
|
const results = await executeDatabaseQuery(kyselyQuery, 'execute');
|
|
419
436
|
const theAll = results.map(r => this.dbResultToDreamInstance(r, this.dreamClass));
|
|
420
437
|
await this.applyPreload(this.query['preloadStatements'], this.query['preloadOnStatements'], theAll);
|
|
@@ -1484,8 +1501,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
1484
1501
|
b = '=';
|
|
1485
1502
|
c = val;
|
|
1486
1503
|
}
|
|
1487
|
-
c = this.normalizedWhereValue(dreamClass, attr, c);
|
|
1488
|
-
c2 = this.normalizedWhereValue(dreamClass, attr, c2);
|
|
1504
|
+
c = maybeBindArrayAsSingleValue(dreamClass, a, b, this.normalizedWhereValue(dreamClass, attr, c));
|
|
1505
|
+
c2 = maybeBindArrayAsSingleValue(dreamClass, a2, b2, this.normalizedWhereValue(dreamClass, attr, c2));
|
|
1489
1506
|
if (a && c === undefined)
|
|
1490
1507
|
throw new CannotPassUndefinedAsAValueToAWhereClause(this.dreamClass, a);
|
|
1491
1508
|
if (a2 && c2 === undefined)
|
|
@@ -2525,6 +2542,31 @@ export function resolvePostgresSsl(connectionConf) {
|
|
|
2525
2542
|
return { rejectUnauthorized: false };
|
|
2526
2543
|
return false;
|
|
2527
2544
|
}
|
|
2545
|
+
const EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE = ['=', '!=', '<>'];
|
|
2546
|
+
/**
|
|
2547
|
+
* Kysely renders a raw Javascript array passed as the right-hand side of a
|
|
2548
|
+
* comparison as a value list (`($1, $2)`), which is what `in` / `not in` need,
|
|
2549
|
+
* but which Postgres rejects for `=` against an array column
|
|
2550
|
+
* (`operator does not exist: text[] = record`). For the equality operators,
|
|
2551
|
+
* bind the array as a single parameter so that `ops.equal([...])` against an
|
|
2552
|
+
* array column means literal array equality (`"col" = $1`).
|
|
2553
|
+
*
|
|
2554
|
+
* The rewrite is gated on the column actually being a database array column.
|
|
2555
|
+
* An array reaching an equality operator against a *scalar* column is a
|
|
2556
|
+
* mistake, and binding it as a single value would turn that mistake into a
|
|
2557
|
+
* clause that matches nothing (`"name" = '{}'`) instead of one that raises, so
|
|
2558
|
+
* it throws instead.
|
|
2559
|
+
*/
|
|
2560
|
+
function maybeBindArrayAsSingleValue(dreamClass, column, operator, val) {
|
|
2561
|
+
if (!Array.isArray(val))
|
|
2562
|
+
return val;
|
|
2563
|
+
if (!EQUALITY_OPERATORS_TAKING_A_WHOLE_ARRAY_VALUE.includes(operator))
|
|
2564
|
+
return val;
|
|
2565
|
+
const isolatedColumn = maybeNamespacedColumnNameToColumnName(column ?? '');
|
|
2566
|
+
if (!isDatabaseArrayColumn(dreamClass, isolatedColumn))
|
|
2567
|
+
throw new ArrayEqualityRequiresArrayColumn(dreamClass, isolatedColumn, operator);
|
|
2568
|
+
return sql.val(val);
|
|
2569
|
+
}
|
|
2528
2570
|
function shouldSoftDelete(dream, reallyDestroy) {
|
|
2529
2571
|
const dreamClass = dream.constructor;
|
|
2530
2572
|
return dreamClass['softDelete'] && !reallyDestroy;
|
|
@@ -198,6 +198,27 @@ export default class PostgresQueryDriver extends KyselyQueryDriver {
|
|
|
198
198
|
await client.end();
|
|
199
199
|
DreamCLI.logger.logEndProgress();
|
|
200
200
|
}
|
|
201
|
+
/**
|
|
202
|
+
* @internal
|
|
203
|
+
*
|
|
204
|
+
* Postgres implementation of the row-locking seam: appends
|
|
205
|
+
* `FOR UPDATE OF "<tableAlias>"` to the select, so every row the statement
|
|
206
|
+
* returns is exclusively locked until the enclosing transaction ends.
|
|
207
|
+
*
|
|
208
|
+
* Scoping the lock to the Query's own base table (rather than a bare
|
|
209
|
+
* `FOR UPDATE`) means a query that joins other tables neither locks rows the
|
|
210
|
+
* caller never asked about nor raises Postgres's "FOR UPDATE cannot be
|
|
211
|
+
* applied to the nullable side of an outer join".
|
|
212
|
+
*
|
|
213
|
+
* This is a waiting lock: neither `NOWAIT` nor `SKIP LOCKED` is applied, so a
|
|
214
|
+
* statement that collides with another transaction's lock blocks until that
|
|
215
|
+
* transaction ends. Callers that cannot afford an unbounded wait should set a
|
|
216
|
+
* `lock_timeout` on the connection; see the guarded-destroy notes on
|
|
217
|
+
* `Query#destroy`.
|
|
218
|
+
*/
|
|
219
|
+
applyRowLock(kyselyQuery, tableAlias) {
|
|
220
|
+
return kyselyQuery.forUpdate(tableAlias);
|
|
221
|
+
}
|
|
201
222
|
static supportsParallelTestDatabases = true;
|
|
202
223
|
/**
|
|
203
224
|
* @internal
|
|
@@ -1,20 +1,69 @@
|
|
|
1
|
+
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Marks a Dream instance as one whose `dependent: 'destroy'` associations were
|
|
6
|
+
* hydrated by this cascade's own call to `loadDependentDestroyTree`, using the
|
|
7
|
+
* same destroy options the cascade is running under.
|
|
8
|
+
*
|
|
9
|
+
* Associations a caller happened to preload themselves (e.g.
|
|
10
|
+
* `Post.preload('comments')`) are deliberately NOT trusted: they may have been
|
|
11
|
+
* loaded under different default scopes than the destroy requires — most
|
|
12
|
+
* importantly, `reallyDestroy` strips the soft-delete scope, so a
|
|
13
|
+
* caller-preloaded association could be missing soft-deleted records that the
|
|
14
|
+
* cascade must destroy.
|
|
15
|
+
*/
|
|
16
|
+
const CASCADE_LOADED = Symbol.for('dream:dependentDestroyTreeLoaded');
|
|
1
17
|
/**
|
|
2
18
|
* @internal
|
|
3
19
|
*
|
|
4
20
|
* Destroys all HasOne/HasMany associations on this
|
|
5
21
|
* dream that are marked as `dependent: 'destroy'`.
|
|
6
22
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
23
|
+
* The whole dependent-destroy tree is loaded ONCE, at the root of the destroy
|
|
24
|
+
* operation, by `loadDependentDestroyTree`. Descendants reached by recursing
|
|
25
|
+
* through this function consume those already-hydrated associations rather
|
|
26
|
+
* than re-querying their own subtrees.
|
|
27
|
+
*
|
|
28
|
+
* A load still happens here when an association is not hydrated. That covers
|
|
29
|
+
* two cases:
|
|
30
|
+
*
|
|
31
|
+
* 1. the root of the operation, whose associations have not been loaded yet
|
|
32
|
+
* 2. a node past `RECURSIVE_DESTROY_PRELOAD_MAX_REPEATS` levels of
|
|
33
|
+
* self-reference, which the root's preload could not reach
|
|
34
|
+
*
|
|
35
|
+
* The second case is why the check is per-association-hydration rather than a
|
|
36
|
+
* "descendants are already loaded" flag: such a flag would truncate cascades
|
|
37
|
+
* deeper than the preload horizon.
|
|
38
|
+
*
|
|
39
|
+
* NOTE: reusing a hydrated subtree is only sound because the same `options`
|
|
40
|
+
* are forwarded unchanged all the way down the cascade (see the recursive
|
|
41
|
+
* `destroy`/`reallyDestroy` calls below), so every node's associations were
|
|
42
|
+
* loaded under the scopes this node would have used. Nothing in the types
|
|
43
|
+
* enforces that; if `options` ever start varying by depth, this reuse has to
|
|
44
|
+
* be revisited.
|
|
10
45
|
*/
|
|
11
46
|
export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
12
47
|
const dreamClass = dream.constructor;
|
|
13
48
|
const { reallyDestroy } = options;
|
|
49
|
+
// `loadDependentDestroyTree` hydrates and returns a CLONE rather than
|
|
50
|
+
// mutating the receiver, so the result must be assigned back — otherwise
|
|
51
|
+
// each association in the loop below would trigger its own reload.
|
|
52
|
+
let dreamWithAssociations = dream;
|
|
53
|
+
let alreadyLoadedTree = false;
|
|
14
54
|
for (const associationName of dreamClass['dependentDestroyAssociationNames']()) {
|
|
15
|
-
|
|
55
|
+
if (!alreadyLoadedTree && !cascadeLoaded(dreamWithAssociations, associationName)) {
|
|
56
|
+
dreamWithAssociations = await loadDependentDestroyTree(dreamWithAssociations, txn, {
|
|
57
|
+
reallyDestroy,
|
|
58
|
+
bypassAllDefaultScopes: options.bypassAllDefaultScopes ?? false,
|
|
59
|
+
defaultScopesToBypass: options.defaultScopesToBypass ?? [],
|
|
60
|
+
});
|
|
61
|
+
alreadyLoadedTree = true;
|
|
62
|
+
}
|
|
63
|
+
const loaded = dreamWithAssociations[associationName];
|
|
16
64
|
const records = Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
17
65
|
for (const record of records) {
|
|
66
|
+
markCascadeLoaded(record);
|
|
18
67
|
if (reallyDestroy) {
|
|
19
68
|
await record.txn(txn).reallyDestroy(options);
|
|
20
69
|
}
|
|
@@ -24,3 +73,26 @@ export default async function destroyAssociatedRecords(dream, txn, options) {
|
|
|
24
73
|
}
|
|
25
74
|
}
|
|
26
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
*
|
|
79
|
+
* True when this dream was hydrated by this cascade's own preload AND the
|
|
80
|
+
* given association is hydrated on it.
|
|
81
|
+
*/
|
|
82
|
+
function cascadeLoaded(dream, associationName) {
|
|
83
|
+
if (!dream[CASCADE_LOADED])
|
|
84
|
+
return false;
|
|
85
|
+
return dream.loaded(associationName);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*
|
|
90
|
+
* A plain assignment rather than `Object.defineProperty`: the key is a symbol,
|
|
91
|
+
* so it is already excluded from `Object.keys`, `for...in` and
|
|
92
|
+
* `JSON.stringify` without an explicit descriptor, and this runs once per node
|
|
93
|
+
* of a potentially wide cascade tree.
|
|
94
|
+
*/
|
|
95
|
+
function markCascadeLoaded(dream) {
|
|
96
|
+
;
|
|
97
|
+
dream[CASCADE_LOADED] = true;
|
|
98
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import DreamApp from '../../dream-app/index.js';
|
|
2
2
|
import destroyAssociatedRecords from './destroyAssociatedRecords.js';
|
|
3
|
-
import loadDependentDestroyTree from './loadDependentDestroyTree.js';
|
|
4
3
|
import runHooksFor from './runHooksFor.js';
|
|
5
4
|
/**
|
|
6
5
|
* @internal
|
|
@@ -31,12 +30,12 @@ export default async function destroyDream(dream, txn = null, options) {
|
|
|
31
30
|
async function destroyDreamWithTransaction(dream, txn, options) {
|
|
32
31
|
const { cascade, reallyDestroy, skipHooks } = options;
|
|
33
32
|
if (cascade) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
await destroyAssociatedRecords(
|
|
33
|
+
// NOTE: the dependent-destroy tree is loaded lazily, inside
|
|
34
|
+
// destroyAssociatedRecords, so that a cascaded descendant whose
|
|
35
|
+
// associations the root already preloaded does not re-query its own
|
|
36
|
+
// subtree. `dream` itself is intentionally never replaced by the loaded
|
|
37
|
+
// clone: the hooks and the delete below operate on the original.
|
|
38
|
+
await destroyAssociatedRecords(dream, txn, options);
|
|
40
39
|
}
|
|
41
40
|
if (!skipHooks) {
|
|
42
41
|
await runHooksFor('beforeDestroy', dream, true, null, txn);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default class InvalidBatchSize extends Error {
|
|
2
|
+
batchSize;
|
|
3
|
+
constructor(batchSize) {
|
|
4
|
+
super();
|
|
5
|
+
this.batchSize = batchSize;
|
|
6
|
+
}
|
|
7
|
+
get message() {
|
|
8
|
+
return `
|
|
9
|
+
Invalid batchSize provided.
|
|
10
|
+
|
|
11
|
+
batchSize given:
|
|
12
|
+
${String(this.batchSize)}
|
|
13
|
+
|
|
14
|
+
batchSize must be a positive integer. A batchSize of 0 is especially dangerous,
|
|
15
|
+
since a limit of 0 is treated as "no limit", which would collapse a batched
|
|
16
|
+
operation into a single unbounded pass over the entire matched set.
|
|
17
|
+
`;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -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,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithDistinct extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// `SELECT DISTINCT ON (...) ... FOR UPDATE` is rejected by the database
|
|
4
|
+
// ("FOR UPDATE is not allowed with DISTINCT clause"), so fail here, with a
|
|
5
|
+
// message that names the two incompatible parts, rather than surfacing a
|
|
6
|
+
// raw driver error from inside a per-batch transaction.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also calls \`distinct\`.
|
|
9
|
+
|
|
10
|
+
A locked read cannot be combined with DISTINCT. Drop the \`distinct\` call, or
|
|
11
|
+
drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export default class RowLockIncompatibleWithJoinLoad extends Error {
|
|
2
|
+
get message() {
|
|
3
|
+
// A join load builds its own select statement (one row per joined
|
|
4
|
+
// association row, later collapsed in memory), which cannot carry the
|
|
5
|
+
// Query's row lock. Rather than silently performing an unlocked read and
|
|
6
|
+
// dropping the compare-and-set guarantee the caller asked for, refuse.
|
|
7
|
+
return `
|
|
8
|
+
An exclusive row lock was requested on a query that also uses leftJoinPreload.
|
|
9
|
+
|
|
10
|
+
A join-loaded query cannot be locked. Use \`preload\` instead of
|
|
11
|
+
\`leftJoinPreload\`, or drop the lock.
|
|
12
|
+
`;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -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,23 @@
|
|
|
1
|
+
export default class ArrayEqualityRequiresArrayColumn extends Error {
|
|
2
|
+
dreamClass;
|
|
3
|
+
column;
|
|
4
|
+
operator;
|
|
5
|
+
constructor(dreamClass, column, operator) {
|
|
6
|
+
super();
|
|
7
|
+
this.dreamClass = dreamClass;
|
|
8
|
+
this.column = column;
|
|
9
|
+
this.operator = operator;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
Attempting to compare an array for equality against a column that is not an
|
|
14
|
+
array in the database:
|
|
15
|
+
|
|
16
|
+
${this.dreamClass.sanitizedName}#${this.column} (operator: ${this.operator})
|
|
17
|
+
|
|
18
|
+
Whole-array equality (\`=\`, \`!=\`, \`<>\`) is only meaningful for array columns.
|
|
19
|
+
If you meant "is this column one of these values", use \`ops.in([...])\`
|
|
20
|
+
(or a bare array) instead.
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -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
|
}
|
|
@@ -117,11 +117,27 @@ const ops = {
|
|
|
117
117
|
/**
|
|
118
118
|
* Creates an `OpsStatement` using the `=` operator for strict equality.
|
|
119
119
|
*
|
|
120
|
+
* **Array columns:** passing an array means literal array equality — the
|
|
121
|
+
* whole array is bound as a single parameter, so the column must match
|
|
122
|
+
* element-for-element, in order. This is the opt-in counterpart to a bare
|
|
123
|
+
* array in a where clause (`Post.where({ tags: ['a', 'b'] })`), which means
|
|
124
|
+
* `IN`, and to `ops.any`, which tests for containment of a single element.
|
|
125
|
+
* Element types are constrained by the column, so an enum array column
|
|
126
|
+
* rejects a string that is not a member of its enum.
|
|
127
|
+
*
|
|
128
|
+
* Note that PostgreSQL defines no equality operator for `json` (as opposed
|
|
129
|
+
* to `jsonb`), so `ops.equal` against a `json[]` column raises at the
|
|
130
|
+
* database.
|
|
131
|
+
*
|
|
120
132
|
* @param equal - The value the column must equal.
|
|
121
133
|
* @returns An `OpsStatement` using the `=` operator.
|
|
122
134
|
*
|
|
123
135
|
* @example
|
|
124
136
|
* User.where({ role: ops.equal('admin') })
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* // matches only rows whose tags column is exactly ['a', 'b'], in that order
|
|
140
|
+
* Post.where({ tags: ops.equal(['a', 'b']) })
|
|
125
141
|
*/
|
|
126
142
|
equal: (equal) => new OpsStatement('=', equal),
|
|
127
143
|
/**
|
|
@@ -260,6 +276,9 @@ const ops = {
|
|
|
260
276
|
/**
|
|
261
277
|
* Creates an `OpsStatement` using the `!=` operator for inequality.
|
|
262
278
|
*
|
|
279
|
+
* **Array columns:** as with `ops.equal`, passing an array compares against
|
|
280
|
+
* the whole array rather than against its elements.
|
|
281
|
+
*
|
|
263
282
|
* @param equal - The value the column must not equal.
|
|
264
283
|
* @returns An `OpsStatement` using the `!=` operator.
|
|
265
284
|
*
|
|
@@ -1011,6 +1011,9 @@ export default class Dream {
|
|
|
1011
1011
|
* // User{email: 'goodbye@world'}
|
|
1012
1012
|
* ```
|
|
1013
1013
|
*
|
|
1014
|
+
* Records are always visited in ascending primary key order; any `order`
|
|
1015
|
+
* applied to the query is discarded. See {@link Query.findEach} for why.
|
|
1016
|
+
*
|
|
1014
1017
|
* @param cb - The callback to call for each found record
|
|
1015
1018
|
* @param opts - Optional parameters for batch processing
|
|
1016
1019
|
* @param opts.batchSize - the batch size you wish to collect records in. If not provided, it will default to 1000
|
|
@@ -14,7 +14,7 @@ export type SpawnOptions = Omit<NodeSpawnOptions, 'shell'> & {
|
|
|
14
14
|
args?: string[];
|
|
15
15
|
};
|
|
16
16
|
export declare const CLI_INDENT = " ";
|
|
17
|
-
export declare const baseColumnsWithTypesDescription = "space separated snake-case properties like this:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
|
|
17
|
+
export declare const baseColumnsWithTypesDescription = "space separated snake-case properties like this:\n title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal\n \n all properties default to not nullable; null can be allowed by appending ':optional':\n subtitle:string:optional\n \n supported types:\n - uuid:\n - uuid[]:\n a column optimized for storing UUIDs\n \n - citext:\n - citext[]:\n case insensitive text (indexes and queries are automatically case insensitive)\n \n - encrypted:\n encrypted text (used in conjunction with the @deco.Encrypted decorator)\n \n - string:\n - string[]:\n varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional\n \n - text\n - text[]\n - date\n - date[]\n - datetime\n - datetime[]\n - time\n - time[]\n - timetz\n - timetz[]\n - integer\n - integer[]\n - boolean\n - boolean[]\n \n - decimal:\n - decimal[]:\n precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: volume:decimal[]:3,2\n \n - enum:\n - enum[]:\n include the enum name to automatically create the enum:\n type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional\n \n omit the enum values to leverage an existing enum (omits the enum type creation):\n type:enum:room_types or type:enum:room_types:optional\n \n leveraging arrays, add the \"[]\" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom";
|
|
18
18
|
export declare const columnsWithTypesDescriptionForStiChild: string;
|
|
19
19
|
export default class DreamCLI {
|
|
20
20
|
/**
|