@rvoh/dream 2.28.0 → 2.28.2
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 +31 -1
- package/dist/cjs/src/decorators/field/Encrypted.js +12 -0
- package/dist/cjs/src/decorators/field/sortable/Sortable.js +10 -0
- package/dist/cjs/src/dream/Query.js +10 -3
- package/dist/cjs/src/encrypt/assertNoEncryptedColumnWrites.js +62 -0
- package/dist/cjs/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/cjs/src/helpers/cli/generateMigration.js +1 -1
- package/dist/cjs/src/helpers/cli/resolveIgnoredColumns.js +2 -2
- package/dist/cjs/src/helpers/cli/writeGeneratedFile.js +1 -1
- package/dist/esm/src/Dream.js +31 -1
- package/dist/esm/src/decorators/field/Encrypted.js +12 -0
- package/dist/esm/src/decorators/field/sortable/Sortable.js +10 -0
- package/dist/esm/src/dream/Query.js +10 -3
- package/dist/esm/src/encrypt/assertNoEncryptedColumnWrites.js +62 -0
- package/dist/esm/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js +31 -0
- package/dist/esm/src/helpers/cli/generateMigration.js +1 -1
- package/dist/esm/src/helpers/cli/resolveIgnoredColumns.js +2 -2
- package/dist/esm/src/helpers/cli/writeGeneratedFile.js +1 -1
- package/dist/types/src/Dream.d.ts +24 -0
- package/dist/types/src/dream/Query.d.ts +6 -2
- package/dist/types/src/encrypt/assertNoEncryptedColumnWrites.d.ts +27 -0
- package/dist/types/src/errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.d.ts +8 -0
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +42 -42
- package/docs/classes/db.PostgresQueryDriver.html +43 -43
- package/docs/classes/db.QueryDriverBase.html +41 -41
- 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.SortableBatchRequiresTooManyScopeLocks.html +3 -3
- package/docs/classes/errors.SortableScopeDidNotStabilize.html +3 -3
- package/docs/classes/errors.SortableScopeLockWaitTimedOut.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 +124 -124
- 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 +95 -79
- 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 +3 -3
package/dist/cjs/src/Dream.js
CHANGED
|
@@ -273,6 +273,15 @@ export default class Dream {
|
|
|
273
273
|
* assignment simply ensures that it is always an array rather than undefined)
|
|
274
274
|
*/
|
|
275
275
|
static explicitUnsafeParamColumns = Object.freeze([]);
|
|
276
|
+
/**
|
|
277
|
+
* @internal
|
|
278
|
+
*
|
|
279
|
+
* Model storage for encrypted attribute metadata, set on the inheriting class when
|
|
280
|
+
* using the Encrypted decorator (this default assignment simply ensures that it is
|
|
281
|
+
* always an array rather than undefined,
|
|
282
|
+
* freezing ensures that we never modify the static array on the inherited Dream class)
|
|
283
|
+
*/
|
|
284
|
+
static encryptedAttributes = Object.freeze([]);
|
|
276
285
|
/**
|
|
277
286
|
* @internal
|
|
278
287
|
*
|
|
@@ -367,6 +376,23 @@ export default class Dream {
|
|
|
367
376
|
static get isSTIBase() {
|
|
368
377
|
return !!this.extendedBy?.length && !this.isSTIChild;
|
|
369
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* @internal
|
|
381
|
+
*
|
|
382
|
+
* This class together with every STI class below it, however deeply nested. These
|
|
383
|
+
* are the classes whose rows a query rooted at this class can reach, since an STI
|
|
384
|
+
* child's rows are a subset of its base's.
|
|
385
|
+
*
|
|
386
|
+
* The classes *above* this one are deliberately excluded, as are its siblings: a
|
|
387
|
+
* column that only a sibling declares is meaningless for the rows this class
|
|
388
|
+
* matches, so treating it as this class's business protects nothing and costs a
|
|
389
|
+
* wider walk on every call.
|
|
390
|
+
*
|
|
391
|
+
* @returns An array of Dream classes
|
|
392
|
+
*/
|
|
393
|
+
static selfAndStiDescendants() {
|
|
394
|
+
return [this, ...(this.extendedBy ?? []).flatMap(dreamClass => dreamClass.selfAndStiDescendants())];
|
|
395
|
+
}
|
|
370
396
|
static stiSiblings() {
|
|
371
397
|
const stiBase = this.stiBaseClassOrOwnClass;
|
|
372
398
|
if (!stiBase.isSTIBase)
|
|
@@ -691,6 +717,10 @@ export default class Dream {
|
|
|
691
717
|
return defaultParams;
|
|
692
718
|
}
|
|
693
719
|
static defaultParamSafeColumns() {
|
|
720
|
+
// `columns()` is table scoped, so it offers this class every column of the shared
|
|
721
|
+
// STI table, including those an STI child marks unsafe. A column unsafe for a child
|
|
722
|
+
// is unsafe here, because a query rooted at this class reaches that child's rows.
|
|
723
|
+
const explicitUnsafeParamColumns = new Set(this.selfAndStiDescendants().flatMap(dreamClass => [...dreamClass.explicitUnsafeParamColumns]));
|
|
694
724
|
const columns = [...this.columns()].filter(column => {
|
|
695
725
|
if (this.prototype._primaryKey === column)
|
|
696
726
|
return false;
|
|
@@ -700,7 +730,7 @@ export default class Dream {
|
|
|
700
730
|
return false;
|
|
701
731
|
if (this.prototype._deletedAtField === column)
|
|
702
732
|
return false;
|
|
703
|
-
if (
|
|
733
|
+
if (explicitUnsafeParamColumns.has(column))
|
|
704
734
|
return false;
|
|
705
735
|
if (this.isBelongsToAssociationForeignKey(column, { includeStiSiblings: true }))
|
|
706
736
|
return false;
|
|
@@ -41,6 +41,18 @@ export default function Encrypted(encryptedColumnName) {
|
|
|
41
41
|
}
|
|
42
42
|
;
|
|
43
43
|
dreamClass['explicitUnsafeParamColumns'].push(encryptedKey);
|
|
44
|
+
if (!Object.getOwnPropertyDescriptor(dreamClass, 'encryptedAttributes')) {
|
|
45
|
+
// This pattern allows `encryptedAttributes` on a base STI class and on
|
|
46
|
+
// child STI classes. The new `encryptedAttributes` property will be created
|
|
47
|
+
// on the child STI class, but it will include all the `encryptedAttributes`
|
|
48
|
+
// already declared on the base STI class.
|
|
49
|
+
dreamClass['encryptedAttributes'] = [...dreamClass['encryptedAttributes']];
|
|
50
|
+
}
|
|
51
|
+
;
|
|
52
|
+
dreamClass['encryptedAttributes'].push({
|
|
53
|
+
property: key,
|
|
54
|
+
encryptedColumnName: encryptedKey,
|
|
55
|
+
});
|
|
44
56
|
const dreamPrototype = Object.getPrototypeOf(this);
|
|
45
57
|
Object.defineProperty(dreamPrototype, key, {
|
|
46
58
|
get() {
|
|
@@ -47,6 +47,16 @@ export default function Sortable(opts = {}) {
|
|
|
47
47
|
// already declared on the base STI class.
|
|
48
48
|
dreamClass['sortableFields'] = [...dreamClass['sortableFields']];
|
|
49
49
|
}
|
|
50
|
+
// an STI child inherits its base's sortable fields via the copy above, and
|
|
51
|
+
// the base's field initializers run again while the child is globally
|
|
52
|
+
// initialized, so without this guard the child registers a second config
|
|
53
|
+
// for the same position field whenever the base is initialized first
|
|
54
|
+
// (models are globally initialized in filesystem order). Every duplicate
|
|
55
|
+
// config repeats the child's position work on each save, so positions
|
|
56
|
+
// advance by the number of duplicates instead of by 1
|
|
57
|
+
if (dreamClass['sortableFields'].some(conf => conf.positionField === key)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
50
60
|
// the decorator registers nothing but this metadata: none of a sortable
|
|
51
61
|
// field's runtime work runs as hooks. A save's preparation and position
|
|
52
62
|
// write run as phases in `saveDream` (`prepareSortableFieldsForSave`,
|
|
@@ -2,6 +2,7 @@ import { SOFT_DELETE_SCOPE_NAME } from '../decorators/class/SoftDelete.js';
|
|
|
2
2
|
import acquireStabilizedSortableBatchLocks from '../decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js';
|
|
3
3
|
import { invalidateSortableRowCache } from '../decorators/field/sortable/helpers/sortableRowCache.js';
|
|
4
4
|
import DreamApp from '../dream-app/index.js';
|
|
5
|
+
import assertNoEncryptedColumnWrites from '../encrypt/assertNoEncryptedColumnWrites.js';
|
|
5
6
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
6
7
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
7
8
|
import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
|
|
@@ -2056,7 +2057,7 @@ export default class Query {
|
|
|
2056
2057
|
// truthiness, since a primary key of 0 is a legitimate cursor value that
|
|
2057
2058
|
// would otherwise reset the window to the start of the set
|
|
2058
2059
|
let lastId = undefined;
|
|
2059
|
-
let attemptedCount
|
|
2060
|
+
let attemptedCount;
|
|
2060
2061
|
do {
|
|
2061
2062
|
const batchQuery = lastId === undefined
|
|
2062
2063
|
? orderedQuery
|
|
@@ -2188,7 +2189,7 @@ export default class Query {
|
|
|
2188
2189
|
* use {@link Query.destroy} instead.
|
|
2189
2190
|
*
|
|
2190
2191
|
* ```ts
|
|
2191
|
-
* await User.where({ email: ops.ilike('%burpcollaborator%').delete()
|
|
2192
|
+
* await User.where({ email: ops.ilike('%burpcollaborator%') }).delete()
|
|
2192
2193
|
* // 12
|
|
2193
2194
|
* ```
|
|
2194
2195
|
*
|
|
@@ -2316,7 +2317,13 @@ export default class Query {
|
|
|
2316
2317
|
}, sortableIncomingAttributes);
|
|
2317
2318
|
}
|
|
2318
2319
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2319
|
-
|
|
2320
|
+
// the guard and the driver must read the same values, so the caller's object
|
|
2321
|
+
// is materialized once here and both read the snapshot
|
|
2322
|
+
const snapshot = { ...attributes };
|
|
2323
|
+
// this path compiles to a single UPDATE statement and never instantiates a
|
|
2324
|
+
// model, so the setter that encrypts an @Encrypted property never runs
|
|
2325
|
+
assertNoEncryptedColumnWrites(this.dreamClass, snapshot);
|
|
2326
|
+
return await this.dbDriverInstance().update(snapshot);
|
|
2320
2327
|
}
|
|
2321
2328
|
invertOrder() {
|
|
2322
2329
|
let query = this.clone({ order: null });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import CannotSetEncryptedColumnInQueryUpdate from '../errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js';
|
|
2
|
+
import { snakeifyString } from '../helpers/snakeify.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Throws when `attributes` names an @Encrypted backing column with anything other
|
|
7
|
+
* than null. Guards the write paths that never instantiate a model, and so never
|
|
8
|
+
* reach the throwing setter the Encrypted decorator installs for that column.
|
|
9
|
+
*
|
|
10
|
+
* `encryptedAttributes` is class-scoped, but a query rooted at an STI base reaches
|
|
11
|
+
* every one of its children's rows on the shared table, so `dreamClass` and every STI
|
|
12
|
+
* class below it are searched for backing columns.
|
|
13
|
+
*
|
|
14
|
+
* The search deliberately stops there rather than covering the whole hierarchy. A query
|
|
15
|
+
* rooted at an STI child matches only that child's rows, and a backing column only a
|
|
16
|
+
* sibling declares holds nothing meaningful for them: writing plaintext into it corrupts
|
|
17
|
+
* no ciphertext, because there is none to corrupt. Converting such a row to the sibling
|
|
18
|
+
* type is what has to clear that column, and it has to clear it whatever this guard did.
|
|
19
|
+
* Widening the walk to the siblings therefore buys no protection, and this guard runs on
|
|
20
|
+
* every single-statement update, so keep it narrow.
|
|
21
|
+
*
|
|
22
|
+
* Keys are matched in their snakeified form, since Kysely's CamelCasePlugin
|
|
23
|
+
* resolves both the camelCase and the snake_case spelling of a key to the same
|
|
24
|
+
* physical column.
|
|
25
|
+
*
|
|
26
|
+
* null is permitted: it clears the column, and there is no plaintext to mangle.
|
|
27
|
+
*/
|
|
28
|
+
export default function assertNoEncryptedColumnWrites(dreamClass, attributes) {
|
|
29
|
+
const encryptedAttributes = encryptedAttributesInStiDescendants(dreamClass);
|
|
30
|
+
if (encryptedAttributes.size === 0)
|
|
31
|
+
return;
|
|
32
|
+
Object.keys(attributes).forEach(key => {
|
|
33
|
+
const encryptedAttribute = encryptedAttributes.get(snakeifyString(key));
|
|
34
|
+
if (encryptedAttribute === undefined)
|
|
35
|
+
return;
|
|
36
|
+
if (attributes[key] === null)
|
|
37
|
+
return;
|
|
38
|
+
throw new CannotSetEncryptedColumnInQueryUpdate(encryptedAttribute.declaringClass, encryptedAttribute.encryptedColumnName, encryptedAttribute.property);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*
|
|
44
|
+
* Every @Encrypted backing column declared on `dreamClass` or on an STI class below it,
|
|
45
|
+
* keyed by the snakeified backing column name.
|
|
46
|
+
*/
|
|
47
|
+
function encryptedAttributesInStiDescendants(dreamClass) {
|
|
48
|
+
const encryptedAttributes = new Map();
|
|
49
|
+
dreamClass['selfAndStiDescendants']().forEach(stiClass => {
|
|
50
|
+
stiClass['encryptedAttributes'].forEach(({ property, encryptedColumnName }) => {
|
|
51
|
+
const snakeifiedColumnName = snakeifyString(encryptedColumnName);
|
|
52
|
+
if (encryptedAttributes.has(snakeifiedColumnName))
|
|
53
|
+
return;
|
|
54
|
+
encryptedAttributes.set(snakeifiedColumnName, {
|
|
55
|
+
property,
|
|
56
|
+
encryptedColumnName,
|
|
57
|
+
declaringClass: stiClass,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return encryptedAttributes;
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default class CannotSetEncryptedColumnInQueryUpdate extends Error {
|
|
2
|
+
dreamClass;
|
|
3
|
+
encryptedColumnName;
|
|
4
|
+
encryptedProperty;
|
|
5
|
+
constructor(dreamClass, encryptedColumnName, encryptedProperty) {
|
|
6
|
+
super();
|
|
7
|
+
this.dreamClass = dreamClass;
|
|
8
|
+
this.encryptedColumnName = encryptedColumnName;
|
|
9
|
+
this.encryptedProperty = encryptedProperty;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
Cannot write a non-null value to an @Encrypted backing column from a query-level update.
|
|
14
|
+
|
|
15
|
+
Dream class: ${this.dreamClass.sanitizedName}
|
|
16
|
+
Encrypted backing column: ${this.encryptedColumnName}
|
|
17
|
+
Property to write instead: ${this.encryptedProperty}
|
|
18
|
+
|
|
19
|
+
\`update(attributes, { skipHooks: true })\` compiles to a single UPDATE statement and
|
|
20
|
+
never instantiates a model, so nothing encrypts the value on its way to the column:
|
|
21
|
+
"${this.encryptedColumnName}" would be written as plaintext, exactly as passed.
|
|
22
|
+
|
|
23
|
+
Write through the property instead, so that Dream encrypts the value:
|
|
24
|
+
|
|
25
|
+
await ${this.dreamClass.sanitizedName}.where({ ... }).update({ ${this.encryptedProperty}: 'value' })
|
|
26
|
+
await ${this.dreamClass.sanitizedName}.where({ ... }).update({ ${this.encryptedProperty}: 'value' }, { lock: true, skipHooks: true })
|
|
27
|
+
|
|
28
|
+
Setting "${this.encryptedColumnName}" to null is permitted, and is how this path clears the column.
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -52,7 +52,7 @@ export default async function generateMigration({ migrationName, columnsWithType
|
|
|
52
52
|
: path.join(dreamPath('db'), 'migrations', connectionName);
|
|
53
53
|
const fileName = `${migrationVersion()}-${hyphenize(migrationName).replace(/\//g, '-')}.ts`;
|
|
54
54
|
const isSTI = !!fullyQualifiedParentName;
|
|
55
|
-
let content
|
|
55
|
+
let content;
|
|
56
56
|
if (isSTI) {
|
|
57
57
|
content = generateStiMigrationContent({
|
|
58
58
|
table: validTableNameOrThrow(migrationName, snakeify(pluralize(pascalizePath(fullyQualifiedParentName)))),
|
|
@@ -137,8 +137,8 @@ function guardAssociationColumns(ignoredColumns, tableName, allModels) {
|
|
|
137
137
|
// pass too, and these guards never see the association's foreign key;
|
|
138
138
|
// the InvalidComputedForeignKey / ExplicitForeignKeyRequired errors
|
|
139
139
|
// swallowed here resurface at runtime as the backstop.
|
|
140
|
-
let foreignKey
|
|
141
|
-
let foreignKeyTypeColumn
|
|
140
|
+
let foreignKey;
|
|
141
|
+
let foreignKeyTypeColumn;
|
|
142
142
|
try {
|
|
143
143
|
foreignKey = associationMetaData.foreignKey();
|
|
144
144
|
foreignKeyTypeColumn = associationMetaData.polymorphic
|
package/dist/esm/src/Dream.js
CHANGED
|
@@ -273,6 +273,15 @@ export default class Dream {
|
|
|
273
273
|
* assignment simply ensures that it is always an array rather than undefined)
|
|
274
274
|
*/
|
|
275
275
|
static explicitUnsafeParamColumns = Object.freeze([]);
|
|
276
|
+
/**
|
|
277
|
+
* @internal
|
|
278
|
+
*
|
|
279
|
+
* Model storage for encrypted attribute metadata, set on the inheriting class when
|
|
280
|
+
* using the Encrypted decorator (this default assignment simply ensures that it is
|
|
281
|
+
* always an array rather than undefined,
|
|
282
|
+
* freezing ensures that we never modify the static array on the inherited Dream class)
|
|
283
|
+
*/
|
|
284
|
+
static encryptedAttributes = Object.freeze([]);
|
|
276
285
|
/**
|
|
277
286
|
* @internal
|
|
278
287
|
*
|
|
@@ -367,6 +376,23 @@ export default class Dream {
|
|
|
367
376
|
static get isSTIBase() {
|
|
368
377
|
return !!this.extendedBy?.length && !this.isSTIChild;
|
|
369
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* @internal
|
|
381
|
+
*
|
|
382
|
+
* This class together with every STI class below it, however deeply nested. These
|
|
383
|
+
* are the classes whose rows a query rooted at this class can reach, since an STI
|
|
384
|
+
* child's rows are a subset of its base's.
|
|
385
|
+
*
|
|
386
|
+
* The classes *above* this one are deliberately excluded, as are its siblings: a
|
|
387
|
+
* column that only a sibling declares is meaningless for the rows this class
|
|
388
|
+
* matches, so treating it as this class's business protects nothing and costs a
|
|
389
|
+
* wider walk on every call.
|
|
390
|
+
*
|
|
391
|
+
* @returns An array of Dream classes
|
|
392
|
+
*/
|
|
393
|
+
static selfAndStiDescendants() {
|
|
394
|
+
return [this, ...(this.extendedBy ?? []).flatMap(dreamClass => dreamClass.selfAndStiDescendants())];
|
|
395
|
+
}
|
|
370
396
|
static stiSiblings() {
|
|
371
397
|
const stiBase = this.stiBaseClassOrOwnClass;
|
|
372
398
|
if (!stiBase.isSTIBase)
|
|
@@ -691,6 +717,10 @@ export default class Dream {
|
|
|
691
717
|
return defaultParams;
|
|
692
718
|
}
|
|
693
719
|
static defaultParamSafeColumns() {
|
|
720
|
+
// `columns()` is table scoped, so it offers this class every column of the shared
|
|
721
|
+
// STI table, including those an STI child marks unsafe. A column unsafe for a child
|
|
722
|
+
// is unsafe here, because a query rooted at this class reaches that child's rows.
|
|
723
|
+
const explicitUnsafeParamColumns = new Set(this.selfAndStiDescendants().flatMap(dreamClass => [...dreamClass.explicitUnsafeParamColumns]));
|
|
694
724
|
const columns = [...this.columns()].filter(column => {
|
|
695
725
|
if (this.prototype._primaryKey === column)
|
|
696
726
|
return false;
|
|
@@ -700,7 +730,7 @@ export default class Dream {
|
|
|
700
730
|
return false;
|
|
701
731
|
if (this.prototype._deletedAtField === column)
|
|
702
732
|
return false;
|
|
703
|
-
if (
|
|
733
|
+
if (explicitUnsafeParamColumns.has(column))
|
|
704
734
|
return false;
|
|
705
735
|
if (this.isBelongsToAssociationForeignKey(column, { includeStiSiblings: true }))
|
|
706
736
|
return false;
|
|
@@ -41,6 +41,18 @@ export default function Encrypted(encryptedColumnName) {
|
|
|
41
41
|
}
|
|
42
42
|
;
|
|
43
43
|
dreamClass['explicitUnsafeParamColumns'].push(encryptedKey);
|
|
44
|
+
if (!Object.getOwnPropertyDescriptor(dreamClass, 'encryptedAttributes')) {
|
|
45
|
+
// This pattern allows `encryptedAttributes` on a base STI class and on
|
|
46
|
+
// child STI classes. The new `encryptedAttributes` property will be created
|
|
47
|
+
// on the child STI class, but it will include all the `encryptedAttributes`
|
|
48
|
+
// already declared on the base STI class.
|
|
49
|
+
dreamClass['encryptedAttributes'] = [...dreamClass['encryptedAttributes']];
|
|
50
|
+
}
|
|
51
|
+
;
|
|
52
|
+
dreamClass['encryptedAttributes'].push({
|
|
53
|
+
property: key,
|
|
54
|
+
encryptedColumnName: encryptedKey,
|
|
55
|
+
});
|
|
44
56
|
const dreamPrototype = Object.getPrototypeOf(this);
|
|
45
57
|
Object.defineProperty(dreamPrototype, key, {
|
|
46
58
|
get() {
|
|
@@ -47,6 +47,16 @@ export default function Sortable(opts = {}) {
|
|
|
47
47
|
// already declared on the base STI class.
|
|
48
48
|
dreamClass['sortableFields'] = [...dreamClass['sortableFields']];
|
|
49
49
|
}
|
|
50
|
+
// an STI child inherits its base's sortable fields via the copy above, and
|
|
51
|
+
// the base's field initializers run again while the child is globally
|
|
52
|
+
// initialized, so without this guard the child registers a second config
|
|
53
|
+
// for the same position field whenever the base is initialized first
|
|
54
|
+
// (models are globally initialized in filesystem order). Every duplicate
|
|
55
|
+
// config repeats the child's position work on each save, so positions
|
|
56
|
+
// advance by the number of duplicates instead of by 1
|
|
57
|
+
if (dreamClass['sortableFields'].some(conf => conf.positionField === key)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
50
60
|
// the decorator registers nothing but this metadata: none of a sortable
|
|
51
61
|
// field's runtime work runs as hooks. A save's preparation and position
|
|
52
62
|
// write run as phases in `saveDream` (`prepareSortableFieldsForSave`,
|
|
@@ -2,6 +2,7 @@ import { SOFT_DELETE_SCOPE_NAME } from '../decorators/class/SoftDelete.js';
|
|
|
2
2
|
import acquireStabilizedSortableBatchLocks from '../decorators/field/sortable/helpers/acquireStabilizedSortableBatchLocks.js';
|
|
3
3
|
import { invalidateSortableRowCache } from '../decorators/field/sortable/helpers/sortableRowCache.js';
|
|
4
4
|
import DreamApp from '../dream-app/index.js';
|
|
5
|
+
import assertNoEncryptedColumnWrites from '../encrypt/assertNoEncryptedColumnWrites.js';
|
|
5
6
|
import AssociationDeclaredWithoutAssociatedDreamClass from '../errors/associations/AssociationDeclaredWithoutAssociatedDreamClass.js';
|
|
6
7
|
import CannotCallUndestroyOnANonSoftDeleteModel from '../errors/CannotCallUndestroyOnANonSoftDeleteModel.js';
|
|
7
8
|
import CannotPassAdditionalFieldsToPluckEachAfterCallback from '../errors/CannotPassAdditionalFieldsToPluckEachAfterCallback.js';
|
|
@@ -2056,7 +2057,7 @@ export default class Query {
|
|
|
2056
2057
|
// truthiness, since a primary key of 0 is a legitimate cursor value that
|
|
2057
2058
|
// would otherwise reset the window to the start of the set
|
|
2058
2059
|
let lastId = undefined;
|
|
2059
|
-
let attemptedCount
|
|
2060
|
+
let attemptedCount;
|
|
2060
2061
|
do {
|
|
2061
2062
|
const batchQuery = lastId === undefined
|
|
2062
2063
|
? orderedQuery
|
|
@@ -2188,7 +2189,7 @@ export default class Query {
|
|
|
2188
2189
|
* use {@link Query.destroy} instead.
|
|
2189
2190
|
*
|
|
2190
2191
|
* ```ts
|
|
2191
|
-
* await User.where({ email: ops.ilike('%burpcollaborator%').delete()
|
|
2192
|
+
* await User.where({ email: ops.ilike('%burpcollaborator%') }).delete()
|
|
2192
2193
|
* // 12
|
|
2193
2194
|
* ```
|
|
2194
2195
|
*
|
|
@@ -2316,7 +2317,13 @@ export default class Query {
|
|
|
2316
2317
|
}, sortableIncomingAttributes);
|
|
2317
2318
|
}
|
|
2318
2319
|
async updateWithoutCallingModelHooks(attributes) {
|
|
2319
|
-
|
|
2320
|
+
// the guard and the driver must read the same values, so the caller's object
|
|
2321
|
+
// is materialized once here and both read the snapshot
|
|
2322
|
+
const snapshot = { ...attributes };
|
|
2323
|
+
// this path compiles to a single UPDATE statement and never instantiates a
|
|
2324
|
+
// model, so the setter that encrypts an @Encrypted property never runs
|
|
2325
|
+
assertNoEncryptedColumnWrites(this.dreamClass, snapshot);
|
|
2326
|
+
return await this.dbDriverInstance().update(snapshot);
|
|
2320
2327
|
}
|
|
2321
2328
|
invertOrder() {
|
|
2322
2329
|
let query = this.clone({ order: null });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import CannotSetEncryptedColumnInQueryUpdate from '../errors/encrypt/CannotSetEncryptedColumnInQueryUpdate.js';
|
|
2
|
+
import { snakeifyString } from '../helpers/snakeify.js';
|
|
3
|
+
/**
|
|
4
|
+
* @internal
|
|
5
|
+
*
|
|
6
|
+
* Throws when `attributes` names an @Encrypted backing column with anything other
|
|
7
|
+
* than null. Guards the write paths that never instantiate a model, and so never
|
|
8
|
+
* reach the throwing setter the Encrypted decorator installs for that column.
|
|
9
|
+
*
|
|
10
|
+
* `encryptedAttributes` is class-scoped, but a query rooted at an STI base reaches
|
|
11
|
+
* every one of its children's rows on the shared table, so `dreamClass` and every STI
|
|
12
|
+
* class below it are searched for backing columns.
|
|
13
|
+
*
|
|
14
|
+
* The search deliberately stops there rather than covering the whole hierarchy. A query
|
|
15
|
+
* rooted at an STI child matches only that child's rows, and a backing column only a
|
|
16
|
+
* sibling declares holds nothing meaningful for them: writing plaintext into it corrupts
|
|
17
|
+
* no ciphertext, because there is none to corrupt. Converting such a row to the sibling
|
|
18
|
+
* type is what has to clear that column, and it has to clear it whatever this guard did.
|
|
19
|
+
* Widening the walk to the siblings therefore buys no protection, and this guard runs on
|
|
20
|
+
* every single-statement update, so keep it narrow.
|
|
21
|
+
*
|
|
22
|
+
* Keys are matched in their snakeified form, since Kysely's CamelCasePlugin
|
|
23
|
+
* resolves both the camelCase and the snake_case spelling of a key to the same
|
|
24
|
+
* physical column.
|
|
25
|
+
*
|
|
26
|
+
* null is permitted: it clears the column, and there is no plaintext to mangle.
|
|
27
|
+
*/
|
|
28
|
+
export default function assertNoEncryptedColumnWrites(dreamClass, attributes) {
|
|
29
|
+
const encryptedAttributes = encryptedAttributesInStiDescendants(dreamClass);
|
|
30
|
+
if (encryptedAttributes.size === 0)
|
|
31
|
+
return;
|
|
32
|
+
Object.keys(attributes).forEach(key => {
|
|
33
|
+
const encryptedAttribute = encryptedAttributes.get(snakeifyString(key));
|
|
34
|
+
if (encryptedAttribute === undefined)
|
|
35
|
+
return;
|
|
36
|
+
if (attributes[key] === null)
|
|
37
|
+
return;
|
|
38
|
+
throw new CannotSetEncryptedColumnInQueryUpdate(encryptedAttribute.declaringClass, encryptedAttribute.encryptedColumnName, encryptedAttribute.property);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* @internal
|
|
43
|
+
*
|
|
44
|
+
* Every @Encrypted backing column declared on `dreamClass` or on an STI class below it,
|
|
45
|
+
* keyed by the snakeified backing column name.
|
|
46
|
+
*/
|
|
47
|
+
function encryptedAttributesInStiDescendants(dreamClass) {
|
|
48
|
+
const encryptedAttributes = new Map();
|
|
49
|
+
dreamClass['selfAndStiDescendants']().forEach(stiClass => {
|
|
50
|
+
stiClass['encryptedAttributes'].forEach(({ property, encryptedColumnName }) => {
|
|
51
|
+
const snakeifiedColumnName = snakeifyString(encryptedColumnName);
|
|
52
|
+
if (encryptedAttributes.has(snakeifiedColumnName))
|
|
53
|
+
return;
|
|
54
|
+
encryptedAttributes.set(snakeifiedColumnName, {
|
|
55
|
+
property,
|
|
56
|
+
encryptedColumnName,
|
|
57
|
+
declaringClass: stiClass,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
return encryptedAttributes;
|
|
62
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default class CannotSetEncryptedColumnInQueryUpdate extends Error {
|
|
2
|
+
dreamClass;
|
|
3
|
+
encryptedColumnName;
|
|
4
|
+
encryptedProperty;
|
|
5
|
+
constructor(dreamClass, encryptedColumnName, encryptedProperty) {
|
|
6
|
+
super();
|
|
7
|
+
this.dreamClass = dreamClass;
|
|
8
|
+
this.encryptedColumnName = encryptedColumnName;
|
|
9
|
+
this.encryptedProperty = encryptedProperty;
|
|
10
|
+
}
|
|
11
|
+
get message() {
|
|
12
|
+
return `
|
|
13
|
+
Cannot write a non-null value to an @Encrypted backing column from a query-level update.
|
|
14
|
+
|
|
15
|
+
Dream class: ${this.dreamClass.sanitizedName}
|
|
16
|
+
Encrypted backing column: ${this.encryptedColumnName}
|
|
17
|
+
Property to write instead: ${this.encryptedProperty}
|
|
18
|
+
|
|
19
|
+
\`update(attributes, { skipHooks: true })\` compiles to a single UPDATE statement and
|
|
20
|
+
never instantiates a model, so nothing encrypts the value on its way to the column:
|
|
21
|
+
"${this.encryptedColumnName}" would be written as plaintext, exactly as passed.
|
|
22
|
+
|
|
23
|
+
Write through the property instead, so that Dream encrypts the value:
|
|
24
|
+
|
|
25
|
+
await ${this.dreamClass.sanitizedName}.where({ ... }).update({ ${this.encryptedProperty}: 'value' })
|
|
26
|
+
await ${this.dreamClass.sanitizedName}.where({ ... }).update({ ${this.encryptedProperty}: 'value' }, { lock: true, skipHooks: true })
|
|
27
|
+
|
|
28
|
+
Setting "${this.encryptedColumnName}" to null is permitted, and is how this path clears the column.
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -52,7 +52,7 @@ export default async function generateMigration({ migrationName, columnsWithType
|
|
|
52
52
|
: path.join(dreamPath('db'), 'migrations', connectionName);
|
|
53
53
|
const fileName = `${migrationVersion()}-${hyphenize(migrationName).replace(/\//g, '-')}.ts`;
|
|
54
54
|
const isSTI = !!fullyQualifiedParentName;
|
|
55
|
-
let content
|
|
55
|
+
let content;
|
|
56
56
|
if (isSTI) {
|
|
57
57
|
content = generateStiMigrationContent({
|
|
58
58
|
table: validTableNameOrThrow(migrationName, snakeify(pluralize(pascalizePath(fullyQualifiedParentName)))),
|
|
@@ -137,8 +137,8 @@ function guardAssociationColumns(ignoredColumns, tableName, allModels) {
|
|
|
137
137
|
// pass too, and these guards never see the association's foreign key;
|
|
138
138
|
// the InvalidComputedForeignKey / ExplicitForeignKeyRequired errors
|
|
139
139
|
// swallowed here resurface at runtime as the backstop.
|
|
140
|
-
let foreignKey
|
|
141
|
-
let foreignKeyTypeColumn
|
|
140
|
+
let foreignKey;
|
|
141
|
+
let foreignKeyTypeColumn;
|
|
142
142
|
try {
|
|
143
143
|
foreignKey = associationMetaData.foreignKey();
|
|
144
144
|
foreignKeyTypeColumn = associationMetaData.polymorphic
|
|
@@ -197,6 +197,15 @@ export default class Dream {
|
|
|
197
197
|
* assignment simply ensures that it is always an array rather than undefined)
|
|
198
198
|
*/
|
|
199
199
|
private static explicitUnsafeParamColumns;
|
|
200
|
+
/**
|
|
201
|
+
* @internal
|
|
202
|
+
*
|
|
203
|
+
* Model storage for encrypted attribute metadata, set on the inheriting class when
|
|
204
|
+
* using the Encrypted decorator (this default assignment simply ensures that it is
|
|
205
|
+
* always an array rather than undefined,
|
|
206
|
+
* freezing ensures that we never modify the static array on the inherited Dream class)
|
|
207
|
+
*/
|
|
208
|
+
private static encryptedAttributes;
|
|
200
209
|
/**
|
|
201
210
|
* @internal
|
|
202
211
|
*
|
|
@@ -283,6 +292,21 @@ export default class Dream {
|
|
|
283
292
|
* @returns boolean
|
|
284
293
|
*/
|
|
285
294
|
private static get isSTIBase();
|
|
295
|
+
/**
|
|
296
|
+
* @internal
|
|
297
|
+
*
|
|
298
|
+
* This class together with every STI class below it, however deeply nested. These
|
|
299
|
+
* are the classes whose rows a query rooted at this class can reach, since an STI
|
|
300
|
+
* child's rows are a subset of its base's.
|
|
301
|
+
*
|
|
302
|
+
* The classes *above* this one are deliberately excluded, as are its siblings: a
|
|
303
|
+
* column that only a sibling declares is meaningless for the rows this class
|
|
304
|
+
* matches, so treating it as this class's business protects nothing and costs a
|
|
305
|
+
* wider walk on every call.
|
|
306
|
+
*
|
|
307
|
+
* @returns An array of Dream classes
|
|
308
|
+
*/
|
|
309
|
+
private static selfAndStiDescendants;
|
|
286
310
|
private static stiSiblings;
|
|
287
311
|
/**
|
|
288
312
|
* @internal
|
|
@@ -1521,7 +1521,7 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
1521
1521
|
* use {@link Query.destroy} instead.
|
|
1522
1522
|
*
|
|
1523
1523
|
* ```ts
|
|
1524
|
-
* await User.where({ email: ops.ilike('%burpcollaborator%').delete()
|
|
1524
|
+
* await User.where({ email: ops.ilike('%burpcollaborator%') }).delete()
|
|
1525
1525
|
* // 12
|
|
1526
1526
|
* ```
|
|
1527
1527
|
*
|
|
@@ -1692,7 +1692,10 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
1692
1692
|
* hanging.
|
|
1693
1693
|
* - **`skipHooks` composes with `lock`, but stays on the per-instance
|
|
1694
1694
|
* path.** With an attributes object and no `lock`, `skipHooks: true`
|
|
1695
|
-
* issues a single `UPDATE ... WHERE` statement that writes raw columns
|
|
1695
|
+
* issues a single `UPDATE ... WHERE` statement that writes raw columns —
|
|
1696
|
+
* with one refusal: it will not write a non-null value into an
|
|
1697
|
+
* `@Encrypted` backing column, since nothing on that path encrypts it, and
|
|
1698
|
+
* throws instead; `null`, which clears the column, is permitted.
|
|
1696
1699
|
* With `lock: true`, `skipHooks: true` skips the model hooks on each
|
|
1697
1700
|
* claimed record but still writes through the instance, so custom setters
|
|
1698
1701
|
* — including the setters encrypted attributes encrypt through — run as
|
|
@@ -1715,6 +1718,7 @@ export default class Query<DreamInstance extends Dream, QueryTypeOpts extends Re
|
|
|
1715
1718
|
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1716
1719
|
* @throws MissingRequiredLockOptionForUpdateCallback if a callback is passed without an options object carrying a boolean `lock`
|
|
1717
1720
|
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
1721
|
+
* @throws CannotSetEncryptedColumnInQueryUpdate if the single-statement `skipHooks` form's attributes name an `@Encrypted` backing column with anything other than `null`
|
|
1718
1722
|
*/
|
|
1719
1723
|
update(cb: (record: DreamInstance) => UpdateableProperties<DreamInstance> | undefined | Promise<UpdateableProperties<DreamInstance> | undefined>, options: {
|
|
1720
1724
|
lock: boolean;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import Dream from '../Dream.js';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* Throws when `attributes` names an @Encrypted backing column with anything other
|
|
6
|
+
* than null. Guards the write paths that never instantiate a model, and so never
|
|
7
|
+
* reach the throwing setter the Encrypted decorator installs for that column.
|
|
8
|
+
*
|
|
9
|
+
* `encryptedAttributes` is class-scoped, but a query rooted at an STI base reaches
|
|
10
|
+
* every one of its children's rows on the shared table, so `dreamClass` and every STI
|
|
11
|
+
* class below it are searched for backing columns.
|
|
12
|
+
*
|
|
13
|
+
* The search deliberately stops there rather than covering the whole hierarchy. A query
|
|
14
|
+
* rooted at an STI child matches only that child's rows, and a backing column only a
|
|
15
|
+
* sibling declares holds nothing meaningful for them: writing plaintext into it corrupts
|
|
16
|
+
* no ciphertext, because there is none to corrupt. Converting such a row to the sibling
|
|
17
|
+
* type is what has to clear that column, and it has to clear it whatever this guard did.
|
|
18
|
+
* Widening the walk to the siblings therefore buys no protection, and this guard runs on
|
|
19
|
+
* every single-statement update, so keep it narrow.
|
|
20
|
+
*
|
|
21
|
+
* Keys are matched in their snakeified form, since Kysely's CamelCasePlugin
|
|
22
|
+
* resolves both the camelCase and the snake_case spelling of a key to the same
|
|
23
|
+
* physical column.
|
|
24
|
+
*
|
|
25
|
+
* null is permitted: it clears the column, and there is no plaintext to mangle.
|
|
26
|
+
*/
|
|
27
|
+
export default function assertNoEncryptedColumnWrites(dreamClass: typeof Dream, attributes: Record<string, unknown>): void;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import Dream from '../../Dream.js';
|
|
2
|
+
export default class CannotSetEncryptedColumnInQueryUpdate extends Error {
|
|
3
|
+
private dreamClass;
|
|
4
|
+
private encryptedColumnName;
|
|
5
|
+
private encryptedProperty;
|
|
6
|
+
constructor(dreamClass: typeof Dream, encryptedColumnName: string, encryptedProperty: string);
|
|
7
|
+
get message(): string;
|
|
8
|
+
}
|