@rvoh/dream 2.29.0 → 2.31.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 +1 -1
- package/dist/cjs/src/decorators/Decorators.js +81 -6
- package/dist/cjs/src/decorators/field/sortable/Sortable.js +4 -31
- package/dist/cjs/src/decorators/field/sortable/helpers/setPosition.js +11 -2
- package/dist/cjs/src/dream/DreamInstanceTransactionBuilder.js +1 -1
- package/dist/cjs/src/dream/Query.js +37 -13
- package/dist/cjs/src/dream/QueryDriver/Kysely.js +73 -33
- package/dist/cjs/src/dream/internal/associations/loadedOrLoadAssociation.js +2 -0
- package/dist/cjs/src/dream/internal/installPersistedAttribute.js +9 -0
- package/dist/cjs/src/dream/internal/instanceMutationOptions.js +1 -0
- package/dist/cjs/src/dream/internal/saveDream.js +9 -0
- package/dist/cjs/src/errors/CannotSaveMissingDream.js +24 -0
- package/dist/cjs/src/errors/SortableBatchRequiresTooManyScopeLocks.js +7 -0
- package/dist/cjs/src/errors/associations/IncompatibleThroughAssociationTarget.js +11 -0
- package/dist/cjs/src/errors/associations/MissingRequiredBelongsToAssociation.js +8 -3
- package/dist/cjs/src/package-exports/errors.js +10 -1
- package/dist/esm/src/Dream.js +1 -1
- package/dist/esm/src/decorators/Decorators.js +81 -6
- package/dist/esm/src/decorators/field/sortable/Sortable.js +4 -31
- package/dist/esm/src/decorators/field/sortable/helpers/setPosition.js +11 -2
- package/dist/esm/src/dream/DreamInstanceTransactionBuilder.js +1 -1
- package/dist/esm/src/dream/Query.js +37 -13
- package/dist/esm/src/dream/QueryDriver/Kysely.js +73 -33
- package/dist/esm/src/dream/internal/associations/loadedOrLoadAssociation.js +2 -0
- package/dist/esm/src/dream/internal/installPersistedAttribute.js +9 -0
- package/dist/esm/src/dream/internal/instanceMutationOptions.js +1 -0
- package/dist/esm/src/dream/internal/saveDream.js +9 -0
- package/dist/esm/src/errors/CannotSaveMissingDream.js +24 -0
- package/dist/esm/src/errors/SortableBatchRequiresTooManyScopeLocks.js +7 -0
- package/dist/esm/src/errors/associations/IncompatibleThroughAssociationTarget.js +11 -0
- package/dist/esm/src/errors/associations/MissingRequiredBelongsToAssociation.js +8 -3
- package/dist/esm/src/package-exports/errors.js +10 -1
- package/dist/types/src/Dream.d.ts +36 -24
- package/dist/types/src/decorators/Decorators.d.ts +81 -6
- package/dist/types/src/decorators/field/sortable/Sortable.d.ts +4 -31
- package/dist/types/src/dream/DreamInstanceTransactionBuilder.d.ts +14 -20
- package/dist/types/src/dream/Query.d.ts +37 -13
- package/dist/types/src/dream/QueryDriver/Kysely.d.ts +1 -1
- package/dist/types/src/dream/internal/associations/loadedOrLoadAssociation.d.ts +2 -0
- package/dist/types/src/dream/internal/installPersistedAttribute.d.ts +6 -0
- package/dist/types/src/dream/internal/instanceMutationOptions.d.ts +13 -0
- package/dist/types/src/errors/CannotSaveMissingDream.d.ts +23 -0
- package/dist/types/src/errors/SortableBatchRequiresTooManyScopeLocks.d.ts +7 -0
- package/dist/types/src/errors/associations/IncompatibleThroughAssociationTarget.d.ts +9 -0
- package/dist/types/src/errors/associations/MissingRequiredBelongsToAssociation.d.ts +1 -0
- package/dist/types/src/package-exports/errors.d.ts +1 -1
- package/docs/assets/hierarchy.js +1 -1
- package/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/db.DreamMigrationHelpers.html +11 -11
- package/docs/classes/db.KyselyQueryDriver.html +43 -43
- package/docs/classes/db.PostgresQueryDriver.html +44 -44
- package/docs/classes/db.QueryDriverBase.html +42 -42
- package/docs/classes/errors.CannotSaveMissingDream.html +47 -0
- 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.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 +63 -22
- package/docs/classes/index.Dream.html +145 -141
- 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 +88 -67
- 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/modules/errors.html +1 -1
- package/docs/types/openapi.CommonOpenapiSchemaObjectFields.html +1 -1
- package/docs/types/openapi.OpenapiAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiFormats.html +1 -1
- package/docs/types/openapi.OpenapiNumberFormats.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArray.html +1 -1
- package/docs/types/openapi.OpenapiSchemaArrayShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBody.html +1 -1
- package/docs/types/openapi.OpenapiSchemaBodyShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaCommonFields.html +1 -1
- package/docs/types/openapi.OpenapiSchemaExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaExpressionRefSchemaShorthand.html +2 -2
- package/docs/types/openapi.OpenapiSchemaInteger.html +1 -1
- package/docs/types/openapi.OpenapiSchemaNull.html +2 -2
- package/docs/types/openapi.OpenapiSchemaNumber.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObject.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAllOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectAnyOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBase.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectBaseShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOf.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectOneOfShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaObjectShorthand.html +1 -1
- package/docs/types/openapi.OpenapiSchemaPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAllOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionAnyOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionOneOf.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializableRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandExpressionSerializerRef.html +2 -2
- package/docs/types/openapi.OpenapiSchemaShorthandPrimitiveGeneric.html +1 -1
- package/docs/types/openapi.OpenapiSchemaString.html +1 -1
- package/docs/types/openapi.OpenapiShorthandAllTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveBaseTypes.html +1 -1
- package/docs/types/openapi.OpenapiShorthandPrimitiveTypes.html +1 -1
- package/docs/types/openapi.OpenapiTypeField.html +1 -1
- package/docs/types/system.DreamAppAllowedPackageManagersEnum.html +1 -1
- package/docs/types/types.CalendarDateDurationUnit.html +1 -1
- package/docs/types/types.CalendarDateObject.html +1 -1
- package/docs/types/types.Camelized.html +1 -1
- package/docs/types/types.ClockTimeObject.html +1 -1
- package/docs/types/types.DbConnectionType.html +1 -1
- package/docs/types/types.DbTypes.html +1 -1
- package/docs/types/types.DreamAssociationMetadata.html +1 -1
- package/docs/types/types.DreamAttributes.html +1 -1
- package/docs/types/types.DreamClassAssociationAndStatement.html +1 -1
- package/docs/types/types.DreamClassColumn.html +1 -1
- package/docs/types/types.DreamColumn.html +1 -1
- package/docs/types/types.DreamColumnNames.html +1 -1
- package/docs/types/types.DreamLogLevel.html +1 -1
- package/docs/types/types.DreamLogger.html +2 -2
- package/docs/types/types.DreamModelSerializerType.html +1 -1
- package/docs/types/types.DreamOrViewModelClassSerializerKey.html +1 -1
- package/docs/types/types.DreamOrViewModelSerializerKey.html +1 -1
- package/docs/types/types.DreamParamSafeAttributes.html +1 -1
- package/docs/types/types.DreamParamSafeColumnNames.html +1 -1
- package/docs/types/types.DreamSerializable.html +1 -1
- package/docs/types/types.DreamSerializableArray.html +1 -1
- package/docs/types/types.DreamSerializerKey.html +1 -1
- package/docs/types/types.DreamSerializers.html +1 -1
- package/docs/types/types.DreamVirtualColumns.html +1 -1
- package/docs/types/types.DurationUnit.html +1 -1
- package/docs/types/types.EncryptAlgorithm.html +1 -1
- package/docs/types/types.HasManyStatement.html +1 -1
- package/docs/types/types.HasOneStatement.html +1 -1
- package/docs/types/types.Hyphenized.html +1 -1
- package/docs/types/types.Pascalized.html +1 -1
- package/docs/types/types.PrimaryKeyType.html +1 -1
- package/docs/types/types.RoundingPrecision.html +1 -1
- package/docs/types/types.SerializerCasing.html +1 -1
- package/docs/types/types.SimpleObjectSerializerType.html +1 -1
- package/docs/types/types.Snakeified.html +1 -1
- package/docs/types/types.StrictInterface.html +1 -1
- package/docs/types/types.UpdateableAssociationProperties.html +1 -1
- package/docs/types/types.UpdateableProperties.html +1 -1
- package/docs/types/types.ValidationType.html +1 -1
- package/docs/types/types.ViewModel.html +2 -2
- package/docs/types/types.ViewModelClass.html +1 -1
- package/docs/types/types.WeekdayName.html +1 -1
- package/docs/types/types.WhereStatementForDream.html +1 -1
- package/docs/types/types.WhereStatementForDreamClass.html +1 -1
- package/docs/variables/index.DreamConst.html +1 -1
- package/docs/variables/index.ops.html +1 -1
- package/docs/variables/openapi.openapiPrimitiveTypes.html +1 -1
- package/docs/variables/openapi.openapiShorthandPrimitiveTypes.html +1 -1
- package/docs/variables/system.DreamAppAllowedPackageManagersEnumValues.html +1 -1
- package/docs/variables/system.primaryKeyTypes.html +1 -1
- package/package.json +1 -1
- package/docs/classes/errors.SortableBatchRequiresTooManyScopeLocks.html +0 -61
package/dist/cjs/src/Dream.js
CHANGED
|
@@ -16,7 +16,7 @@ import loadedOrLoadAssociation from './dream/internal/associations/loadedOrLoadA
|
|
|
16
16
|
import undestroyAssociation from './dream/internal/associations/undestroyAssociation.js';
|
|
17
17
|
import associationStringToNameAndAlias from './dream/internal/associationStringToNameAndAlias.js';
|
|
18
18
|
import destroyDream from './dream/internal/destroyDream.js';
|
|
19
|
-
import { destroyOptions, reallyDestroyOptions, undestroyOptions
|
|
19
|
+
import { destroyOptions, reallyDestroyOptions, undestroyOptions } from './dream/internal/destroyOptions.js';
|
|
20
20
|
import ensureSTITypeFieldIsSet from './dream/internal/ensureSTITypeFieldIsSet.js';
|
|
21
21
|
import findOrCreateBy from './dream/internal/findOrCreateBy.js';
|
|
22
22
|
import mergeSerializerAssociationEdges from './dream/internal/mergeSerializerAssociationEdges.js';
|
|
@@ -147,18 +147,93 @@ export default class Decorators {
|
|
|
147
147
|
return Scope(opts);
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* NOTE: the Sortable decorator may not be used in STI child models (it may be used in the STI base class)
|
|
150
|
+
* Marks an integer column as a sortable position: Dream keeps the positions of
|
|
151
|
+
* every record in a sort scope contiguous, starting at 1, as records are
|
|
152
|
+
* created, moved, destroyed and undestroyed.
|
|
154
153
|
*
|
|
155
154
|
* ```ts
|
|
156
|
-
* class
|
|
155
|
+
* class Post extends ApplicationModel {
|
|
157
156
|
* @deco.Sortable({ scope: 'user' })
|
|
158
|
-
* public position:
|
|
157
|
+
* public position: number
|
|
159
158
|
* }
|
|
159
|
+
*
|
|
160
|
+
* await post.update({ position: 2 }) // the records at 2 and above shift up
|
|
160
161
|
* ```
|
|
161
162
|
*
|
|
163
|
+
* A position past the end of the scope is clamped to the end, and a position
|
|
164
|
+
* below 1 — or none at all — lands the record at the end.
|
|
165
|
+
*
|
|
166
|
+
* **A save that changes the sort scope ignores a position given alongside it.**
|
|
167
|
+
* The record lands at the end of the scope it moves into, whatever position the
|
|
168
|
+
* same `update` supplied:
|
|
169
|
+
*
|
|
170
|
+
* ```ts
|
|
171
|
+
* await post.update({ user: otherUser, position: 1 })
|
|
172
|
+
* // post is now the last record in otherUser's scope, not the first
|
|
173
|
+
* ```
|
|
174
|
+
*
|
|
175
|
+
* Move it in two saves to place it: `await post.update({ user: otherUser })`,
|
|
176
|
+
* then `await post.update({ position: 1 })`.
|
|
177
|
+
*
|
|
178
|
+
* Sortable requires a query driver that supports advisory transaction locks —
|
|
179
|
+
* the `PostgresQueryDriver` does — since every position write serializes the
|
|
180
|
+
* writers of its sort scope on one. This concurrency guarantee first shipped
|
|
181
|
+
* in Dream 2.28.0 and begins only after every writer is running a lock-aware
|
|
182
|
+
* release. During the first rolling deployment, older processes take no
|
|
183
|
+
* advisory locks and can still race the upgraded processes.
|
|
184
|
+
*
|
|
185
|
+
* All participating Dream writers of one hot scope serialize. Ordinary saves
|
|
186
|
+
* and destroys with `skipHooks`, direct query writes, raw SQL, and older
|
|
187
|
+
* pre-lock Dream processes bypass Sortable maintenance and do not participate
|
|
188
|
+
* in its locking protocol. Undestroy still performs stabilized Sortable
|
|
189
|
+
* maintenance and acquires scope locks with `skipHooks: true`; locked query
|
|
190
|
+
* batches likewise acquire their scope locks during preflight, before any
|
|
191
|
+
* per-record callbacks, even when those callbacks skip hooks. A waiter that
|
|
192
|
+
* enters the protocol holds a pooled connection until the holder finishes or
|
|
193
|
+
* `sortableScopeLockTimeout` expires; sustained contention can therefore
|
|
194
|
+
* produce latency waves, timeouts, and pool starvation. Keep database work
|
|
195
|
+
* inside the lock window short, and avoid cross-region database latency for
|
|
196
|
+
* hot scopes.
|
|
197
|
+
*
|
|
198
|
+
* `SortableScopeDidNotStabilize` and `SortableScopeLockWaitTimedOut` are the
|
|
199
|
+
* expected Dream errors an application may choose to retry. Database deadlocks
|
|
200
|
+
* remain native adapter errors (for example PostgreSQL code `40P01` or MySQL
|
|
201
|
+
* errno `1213`) so their driver fields and stacks remain intact. A database
|
|
202
|
+
* deadlock aborts the whole transaction: retry by starting the transaction
|
|
203
|
+
* again from the beginning, never by continuing it or retrying only the failed
|
|
204
|
+
* statement. Recurrent deadlocks call for shorter transactions or a consistent
|
|
205
|
+
* multi-resource acquisition order; they do not by themselves prove Sortable
|
|
206
|
+
* caused the cycle.
|
|
207
|
+
*
|
|
208
|
+
* ```ts
|
|
209
|
+
* import {
|
|
210
|
+
* SortableScopeDidNotStabilize,
|
|
211
|
+
* SortableScopeLockWaitTimedOut,
|
|
212
|
+
* } from '@rvoh/dream/errors'
|
|
213
|
+
*
|
|
214
|
+
* const runTransaction = async () =>
|
|
215
|
+
* await ApplicationModel.transaction(async txn => {
|
|
216
|
+
* // bind every operation in the attempt to txn
|
|
217
|
+
* })
|
|
218
|
+
*
|
|
219
|
+
* try {
|
|
220
|
+
* await runTransaction()
|
|
221
|
+
* } catch (error) {
|
|
222
|
+
* const adapterDeadlock =
|
|
223
|
+
* (error as { code?: string }).code === '40P01' ||
|
|
224
|
+
* (error as { errno?: number }).errno === 1213
|
|
225
|
+
* const retryable =
|
|
226
|
+
* error instanceof SortableScopeDidNotStabilize ||
|
|
227
|
+
* error instanceof SortableScopeLockWaitTimedOut ||
|
|
228
|
+
* adapterDeadlock
|
|
229
|
+
* if (!retryable) throw error
|
|
230
|
+
*
|
|
231
|
+
* await runTransaction() // one whole-transaction retry
|
|
232
|
+
* }
|
|
233
|
+
* ```
|
|
234
|
+
*
|
|
235
|
+
* NOTE: the Sortable decorator may not be used in STI child models (it may be used in the STI base class)
|
|
236
|
+
*
|
|
162
237
|
* @param opts - Configuration options for the sortable decorator
|
|
163
238
|
* @param opts.scope - The column, association, or combination thereof which you would like to restrict the incrementing logic to. Can be a single column name, a single belongs-to association name, or an array of column/association names
|
|
164
239
|
* @returns A Sortable decorator
|
|
@@ -1,36 +1,9 @@
|
|
|
1
1
|
import scopeArray from './helpers/scopeArray.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* ```ts
|
|
8
|
-
* class Post extends ApplicationModel {
|
|
9
|
-
* @deco.Sortable({ scope: 'user' })
|
|
10
|
-
* public position: number
|
|
11
|
-
* }
|
|
12
|
-
*
|
|
13
|
-
* await post.update({ position: 2 }) // the records at 2 and above shift up
|
|
14
|
-
* ```
|
|
15
|
-
*
|
|
16
|
-
* A position past the end of the scope is clamped to the end, and a position
|
|
17
|
-
* below 1 — or none at all — lands the record at the end.
|
|
18
|
-
*
|
|
19
|
-
* **A save that changes the sort scope ignores a position given alongside it.**
|
|
20
|
-
* The record lands at the end of the scope it moves into, whatever position the
|
|
21
|
-
* same `update` supplied:
|
|
22
|
-
*
|
|
23
|
-
* ```ts
|
|
24
|
-
* await post.update({ user: otherUser, position: 1 })
|
|
25
|
-
* // post is now the last record in otherUser's scope, not the first
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* Move it in two saves to place it: `await post.update({ user: otherUser })`,
|
|
29
|
-
* then `await post.update({ position: 1 })`.
|
|
30
|
-
*
|
|
31
|
-
* Sortable requires a query driver that supports advisory transaction locks —
|
|
32
|
-
* the `PostgresQueryDriver` does — since every position write serializes the
|
|
33
|
-
* writers of its sort scope on one.
|
|
3
|
+
* The implementation behind `@deco.Sortable`. The decorator's semantics —
|
|
4
|
+
* position clamping, what a scope-changing save does with a position given
|
|
5
|
+
* alongside it, and the advisory-lock concurrency protocol — are documented on
|
|
6
|
+
* `Decorators#Sortable`, which is where a consumer reads them.
|
|
34
7
|
*/
|
|
35
8
|
export default function Sortable(opts = {}) {
|
|
36
9
|
return function (_, context) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import installPersistedAttribute from '../../../../dream/internal/installPersistedAttribute.js';
|
|
2
|
+
import RecordNotFound from '../../../../errors/RecordNotFound.js';
|
|
1
3
|
import range from '../../../../helpers/range.js';
|
|
2
4
|
import ops from '../../../../ops/index.js';
|
|
3
5
|
import acquireSortableScopeLocks from './acquireSortableScopeLocks.js';
|
|
@@ -9,9 +11,16 @@ import { invalidateSortableRowCache } from './sortableRowCache.js';
|
|
|
9
11
|
import { snapshotScopeValue } from './sortableSnapshot.js';
|
|
10
12
|
import validPosition from './validPosition.js';
|
|
11
13
|
export default async function setPosition(obj) {
|
|
12
|
-
const { dream, txn } = obj;
|
|
13
14
|
await applyUpdates(obj);
|
|
14
|
-
await
|
|
15
|
+
await refreshPosition(obj);
|
|
16
|
+
}
|
|
17
|
+
async function refreshPosition({ dream, positionField, query }) {
|
|
18
|
+
const positions = await query['removeAllDefaultScopes']()
|
|
19
|
+
.where({ [dream['_primaryKey']]: dream.primaryKeyValue() })
|
|
20
|
+
.pluck(positionField);
|
|
21
|
+
if (!positions.length)
|
|
22
|
+
throw new RecordNotFound(dream['sanitizedConstructorName']);
|
|
23
|
+
installPersistedAttribute(dream, positionField, positions[0]);
|
|
15
24
|
}
|
|
16
25
|
async function applyUpdates(obj) {
|
|
17
26
|
const { dream, positionField, scope, txn } = obj;
|
|
@@ -10,7 +10,7 @@ import destroyAssociation from './internal/associations/destroyAssociation.js';
|
|
|
10
10
|
import loadedOrLoadAssociation from './internal/associations/loadedOrLoadAssociation.js';
|
|
11
11
|
import undestroyAssociation from './internal/associations/undestroyAssociation.js';
|
|
12
12
|
import destroyDream from './internal/destroyDream.js';
|
|
13
|
-
import { destroyOptions, reallyDestroyOptions, undestroyOptions
|
|
13
|
+
import { destroyOptions, reallyDestroyOptions, undestroyOptions } from './internal/destroyOptions.js';
|
|
14
14
|
import reload from './internal/reload.js';
|
|
15
15
|
import saveDream from './internal/saveDream.js';
|
|
16
16
|
import { DEFAULT_BYPASS_ALL_DEFAULT_SCOPES, DEFAULT_DEFAULT_SCOPES_TO_BYPASS, DEFAULT_SKIP_HOOKS, } from './internal/scopeHelpers.js';
|
|
@@ -1914,13 +1914,15 @@ export default class Query {
|
|
|
1914
1914
|
* {@link Query.delete | delete} (a single statement, no hooks or cascade)
|
|
1915
1915
|
* are the right tools for bulk removal.
|
|
1916
1916
|
*
|
|
1917
|
+
* To bypass a default scope, chain `removeDefaultScope` onto the Query
|
|
1918
|
+
* before calling this method; it is Query state, not an option, and it
|
|
1919
|
+
* carries through to the cascade.
|
|
1920
|
+
*
|
|
1917
1921
|
* @param options - Options for destroying the instance
|
|
1918
1922
|
* @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
1919
1923
|
* @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
1920
1924
|
* @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. Defaults to false
|
|
1921
1925
|
* @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
|
|
1922
|
-
* @param options.bypassAllDefaultScopes - If true, bypasses user-removable default scopes when cascade destroying; an STI child's reserved discriminator remains enforced, including when associations are loaded. Defaults to false
|
|
1923
|
-
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
1924
1926
|
* @returns The number of records that were removed
|
|
1925
1927
|
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
1926
1928
|
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
@@ -2034,11 +2036,12 @@ export default class Query {
|
|
|
2034
2036
|
* transaction ends, so every batch's scope keys are still held when the
|
|
2035
2037
|
* next one preflights. That accumulation is bounded — the preflight counts
|
|
2036
2038
|
* the keys the transaction already holds along with the ones the batch
|
|
2037
|
-
* would add, and
|
|
2038
|
-
*
|
|
2039
|
-
*
|
|
2040
|
-
*
|
|
2041
|
-
* releases the keys as it goes and gives
|
|
2039
|
+
* would add, and raises rather than letting a long run over a
|
|
2040
|
+
* high-cardinality sort scope exhaust `max_locks_per_transaction` for the
|
|
2041
|
+
* whole cluster — so a run long enough raises instead of completing. That
|
|
2042
|
+
* refusal is a sizing failure to fix, not a condition to catch: letting
|
|
2043
|
+
* Dream open a transaction per batch releases the keys as it goes and gives
|
|
2044
|
+
* each batch the whole budget, and a smaller `batchSize` takes fewer.
|
|
2042
2045
|
* - the preflight covers **the claimed rows' own scopes only**. A
|
|
2043
2046
|
* `dependent: 'destroy'` cascade under `destroy({ lock: true })` destroys
|
|
2044
2047
|
* child records of other tables, and a sortable child takes its own table's
|
|
@@ -2116,8 +2119,18 @@ export default class Query {
|
|
|
2116
2119
|
}
|
|
2117
2120
|
/**
|
|
2118
2121
|
* Destroys all records matching the Query,
|
|
2119
|
-
*
|
|
2120
|
-
*
|
|
2122
|
+
* permanently removing each matched record from
|
|
2123
|
+
* the database instead of soft deleting it.
|
|
2124
|
+
*
|
|
2125
|
+
* The SoftDelete decorator is bypassed when
|
|
2126
|
+
* deleting each matched record, not when
|
|
2127
|
+
* matching: the Query selects under its default
|
|
2128
|
+
* scopes, `dream:SoftDelete` among them, so on a
|
|
2129
|
+
* SoftDelete model it matches only records that
|
|
2130
|
+
* have not already been soft deleted. Reaching
|
|
2131
|
+
* soft deleted records takes
|
|
2132
|
+
* `removeDefaultScope('dream:SoftDelete')` on
|
|
2133
|
+
* the Query.
|
|
2121
2134
|
*
|
|
2122
2135
|
* Calls model hooks and applies cascade destroy
|
|
2123
2136
|
* to associations with `dependent: 'destroy'`,
|
|
@@ -2132,17 +2145,26 @@ export default class Query {
|
|
|
2132
2145
|
* // 12
|
|
2133
2146
|
* ```
|
|
2134
2147
|
*
|
|
2148
|
+
* ```ts
|
|
2149
|
+
* await User.removeDefaultScope('dream:SoftDelete')
|
|
2150
|
+
* .where({ email: ops.ilike('%burpcollaborator%') })
|
|
2151
|
+
* .reallyDestroy()
|
|
2152
|
+
* // 12
|
|
2153
|
+
* ```
|
|
2154
|
+
*
|
|
2135
2155
|
* A `limit` or `offset` on the Query is incompatible with `reallyDestroy`,
|
|
2136
2156
|
* exactly as it is with {@link Query.destroy}: a limit- or offset-carrying
|
|
2137
2157
|
* Query throws at runtime.
|
|
2138
2158
|
*
|
|
2159
|
+
* To bypass a default scope, chain `removeDefaultScope` onto the Query
|
|
2160
|
+
* before calling this method; it is Query state, not an option, and it
|
|
2161
|
+
* carries through to the cascade.
|
|
2162
|
+
*
|
|
2139
2163
|
* @param options - Options for destroying the instance
|
|
2140
2164
|
* @param options.skipHooks - If true, skips applying model hooks during the destroy operation. Defaults to false
|
|
2141
2165
|
* @param options.cascade - If false, skips destroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
2142
2166
|
* @param options.lock - If true, each batch is re-selected with an exclusive row lock inside its own transaction before being destroyed, making the destroy a compare-and-set. See {@link Query.destroy} for the full semantics. Defaults to false
|
|
2143
2167
|
* @param options.batchSize - The number of records to process per batch. Must be a positive integer. Defaults to 10 when `lock` is true, and to 1000 otherwise
|
|
2144
|
-
* @param options.bypassAllDefaultScopes - If true, bypasses user-removable default scopes when cascade destroying; an STI child's reserved discriminator remains enforced, including when associations are loaded. Defaults to false
|
|
2145
|
-
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade destroying. Defaults to an empty array
|
|
2146
2168
|
* @returns The number of records that were removed
|
|
2147
2169
|
* @throws InvalidBatchSize if `batchSize` is not a positive integer
|
|
2148
2170
|
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
@@ -2168,11 +2190,13 @@ export default class Query {
|
|
|
2168
2190
|
* re-applied to every window — so a limit- or offset-carrying Query throws
|
|
2169
2191
|
* at runtime.
|
|
2170
2192
|
*
|
|
2193
|
+
* To bypass a default scope, chain `removeDefaultScope` onto the Query
|
|
2194
|
+
* before calling this method; it is Query state, not an option, and it
|
|
2195
|
+
* carries through to the cascade. The SoftDelete scope is always bypassed.
|
|
2196
|
+
*
|
|
2171
2197
|
* @param options - Options for undestroying the instance
|
|
2172
2198
|
* @param options.skipHooks - If true, skips applying model hooks during the undestroy operation. Defaults to false
|
|
2173
2199
|
* @param options.cascade - If false, skips undestroying associations marked `dependent: 'destroy'`. Defaults to true
|
|
2174
|
-
* @param options.bypassAllDefaultScopes - If true, bypasses user-removable default scopes when cascade undestroying; an STI child's reserved discriminator remains enforced, including when associations are loaded. Defaults to false
|
|
2175
|
-
* @param options.defaultScopesToBypass - An array of default scope names to bypass when cascade undestroying (soft delete is always bypassed). Defaults to an empty array
|
|
2176
2200
|
* @returns The number of records that were removed
|
|
2177
2201
|
* @throws BatchingIncompatibleWithLimitOrOffset if the query carries a `limit` or `offset`
|
|
2178
2202
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
8
8
|
// @ts-ignore
|
|
9
9
|
import pg from 'pg';
|
|
10
|
-
import { PostgresDialect, sql, } from 'kysely';
|
|
10
|
+
import { NoResultError, PostgresDialect, sql, } from 'kysely';
|
|
11
11
|
import pluralize from 'pluralize-esm';
|
|
12
12
|
import { CliFileWriter } from '../../cli/CliFileWriter.js';
|
|
13
13
|
import DreamCLI from '../../cli/index.js';
|
|
@@ -21,6 +21,7 @@ import DreamApp from '../../dream-app/index.js';
|
|
|
21
21
|
import Dream from '../../Dream.js';
|
|
22
22
|
import ArrayTargetIncompatibleWithThroughAssociation from '../../errors/associations/ArrayTargetIncompatibleWithThroughAssociation.js';
|
|
23
23
|
import CannotJoinPolymorphicBelongsToError from '../../errors/associations/CannotJoinPolymorphicBelongsToError.js';
|
|
24
|
+
import IncompatibleThroughAssociationTarget from '../../errors/associations/IncompatibleThroughAssociationTarget.js';
|
|
24
25
|
import JoinAttemptedOnMissingAssociation from '../../errors/associations/JoinAttemptedOnMissingAssociation.js';
|
|
25
26
|
import MissingRequiredAssociationAndClause from '../../errors/associations/MissingRequiredAssociationAndClause.js';
|
|
26
27
|
import MissingRequiredPassthroughForAssociationAndClause from '../../errors/associations/MissingRequiredPassthroughForAssociationAndClause.js';
|
|
@@ -29,6 +30,7 @@ import MissingThroughAssociationSource from '../../errors/associations/MissingTh
|
|
|
29
30
|
import CannotNegateSimilarityClause from '../../errors/CannotNegateSimilarityClause.js';
|
|
30
31
|
import CannotNamespaceAssociationFilterToAnotherTable from '../../errors/CannotNamespaceAssociationFilterToAnotherTable.js';
|
|
31
32
|
import CannotPassUndefinedAsAValueToAWhereClause from '../../errors/CannotPassUndefinedAsAValueToAWhereClause.js';
|
|
33
|
+
import CannotSaveMissingDream from '../../errors/CannotSaveMissingDream.js';
|
|
32
34
|
import ArrayEqualityRequiresArrayColumn from '../../errors/ops/ArrayEqualityRequiresArrayColumn.js';
|
|
33
35
|
import RowLockIncompatibleWithDistinct from '../../errors/RowLockIncompatibleWithDistinct.js';
|
|
34
36
|
import RowLockIncompatibleWithJoinLoad from '../../errors/RowLockIncompatibleWithJoinLoad.js';
|
|
@@ -805,6 +807,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
805
807
|
}
|
|
806
808
|
}
|
|
807
809
|
catch (error) {
|
|
810
|
+
if (dream.isPersisted && error instanceof NoResultError)
|
|
811
|
+
throw new CannotSaveMissingDream(dream);
|
|
808
812
|
switch (pgErrorType(error)) {
|
|
809
813
|
case COLUMN_OVERFLOW:
|
|
810
814
|
throw new ColumnOverflow(error instanceof Error ? error : new Error('database column overflow'));
|
|
@@ -1547,7 +1551,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
1547
1551
|
joinAndStatement,
|
|
1548
1552
|
joinType,
|
|
1549
1553
|
previousThroughAssociations: [],
|
|
1550
|
-
|
|
1554
|
+
polymorphicSourceTargetModelClass: undefined,
|
|
1551
1555
|
});
|
|
1552
1556
|
query = results.query;
|
|
1553
1557
|
/**
|
|
@@ -1976,7 +1980,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
1976
1980
|
joinAndStatement: {},
|
|
1977
1981
|
previousThroughAssociations: [],
|
|
1978
1982
|
joinType,
|
|
1979
|
-
|
|
1983
|
+
polymorphicSourceTargetModelClass: undefined,
|
|
1980
1984
|
});
|
|
1981
1985
|
}
|
|
1982
1986
|
const associatedDreamClass = associationReferencedByThroughAssociation.modelCB();
|
|
@@ -1992,12 +1996,24 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
1992
1996
|
association: throughAssociation,
|
|
1993
1997
|
throughClass: associatedDreamClass,
|
|
1994
1998
|
});
|
|
1999
|
+
const outermostThroughAssociation = previousThroughAssociations[0]?.association ?? throughAssociation;
|
|
2000
|
+
const outermostTargetModelClass = outermostThroughAssociation.modelCB();
|
|
2001
|
+
const sourceTargetModelClassOrClasses = sourceAssociation.modelCB();
|
|
2002
|
+
const sourceTargetModelClasses = Array.isArray(sourceTargetModelClassOrClasses)
|
|
2003
|
+
? sourceTargetModelClassOrClasses
|
|
2004
|
+
: [sourceTargetModelClassOrClasses];
|
|
2005
|
+
if (!sourceTargetModelClasses.some(sourceTargetModelClass => outermostTargetModelClass.typeof(sourceTargetModelClass)))
|
|
2006
|
+
throw new IncompatibleThroughAssociationTarget({
|
|
2007
|
+
outermostAssociation: outermostThroughAssociation,
|
|
2008
|
+
outermostTargetModelClass,
|
|
2009
|
+
sourceAssociation,
|
|
2010
|
+
});
|
|
1995
2011
|
/**
|
|
1996
|
-
*
|
|
1997
|
-
* association
|
|
2012
|
+
* A polymorphic source can accept several model classes. The outermost
|
|
2013
|
+
* association target selects the one this through chain is following.
|
|
1998
2014
|
*/
|
|
1999
|
-
const
|
|
2000
|
-
?
|
|
2015
|
+
const polymorphicSourceTargetModelClass = Array.isArray(sourceTargetModelClassOrClasses)
|
|
2016
|
+
? outermostTargetModelClass
|
|
2001
2017
|
: undefined;
|
|
2002
2018
|
/**
|
|
2003
2019
|
* Add the source that `sourceAssociation` points to. Note that since joinsBridgeThroughAssociations
|
|
@@ -2031,7 +2047,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2031
2047
|
{ association: throughAssociation, selfTableAlias: previousTableAlias },
|
|
2032
2048
|
],
|
|
2033
2049
|
joinType,
|
|
2034
|
-
|
|
2050
|
+
polymorphicSourceTargetModelClass,
|
|
2035
2051
|
});
|
|
2036
2052
|
}
|
|
2037
2053
|
applyOneJoin({ query, dreamClass, association,
|
|
@@ -2054,7 +2070,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2054
2070
|
/**
|
|
2055
2071
|
* previousTableAlias is always set
|
|
2056
2072
|
*/
|
|
2057
|
-
previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociations, joinType,
|
|
2073
|
+
previousTableAlias, selfTableAlias = previousTableAlias, joinAndStatement = {}, previousThroughAssociations, joinType, polymorphicSourceTargetModelClass, }) {
|
|
2058
2074
|
if (association.type !== 'BelongsTo' && association.through) {
|
|
2059
2075
|
return this.joinsBridgeThroughAssociations({
|
|
2060
2076
|
query,
|
|
@@ -2077,7 +2093,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2077
2093
|
joinAndStatement,
|
|
2078
2094
|
previousThroughAssociations,
|
|
2079
2095
|
joinType,
|
|
2080
|
-
|
|
2096
|
+
polymorphicSourceTargetModelClass,
|
|
2081
2097
|
});
|
|
2082
2098
|
}
|
|
2083
2099
|
addAssociationJoinStatementToQuery({ query, dreamClass, association,
|
|
@@ -2100,12 +2116,38 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2100
2116
|
/**
|
|
2101
2117
|
* previousTableAlias is always set
|
|
2102
2118
|
*/
|
|
2103
|
-
previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociations, joinType,
|
|
2119
|
+
previousTableAlias, selfTableAlias, joinAndStatement, previousThroughAssociations, joinType, polymorphicSourceTargetModelClass, }) {
|
|
2104
2120
|
const currentTableAlias = explicitAlias ?? association.as;
|
|
2105
|
-
const
|
|
2106
|
-
const
|
|
2107
|
-
?
|
|
2108
|
-
:
|
|
2121
|
+
const associationModelClassOrPolymorphicModelClasses = association.modelCB();
|
|
2122
|
+
const modelClassUsedForJoinConditions = Array.isArray(associationModelClassOrPolymorphicModelClasses)
|
|
2123
|
+
? associationModelClassOrPolymorphicModelClasses[0]
|
|
2124
|
+
: associationModelClassOrPolymorphicModelClasses;
|
|
2125
|
+
/**
|
|
2126
|
+
* The outermost through association defines what callers expect this join to
|
|
2127
|
+
* return, so its target is the single owner of the target table's default
|
|
2128
|
+
* scopes. Inner associations describe how to reach that table; the
|
|
2129
|
+
* compatibility check in `joinsBridgeThroughAssociations` ensures they can
|
|
2130
|
+
* actually return the outer target.
|
|
2131
|
+
*
|
|
2132
|
+
* For example, BearBnB already defines `Host.places` and `Place.rooms`. It
|
|
2133
|
+
* could expose only bedrooms from a host without adding `Place.bedrooms`:
|
|
2134
|
+
*
|
|
2135
|
+
* // Place.ts
|
|
2136
|
+
* @deco.HasMany('Room')
|
|
2137
|
+
* public rooms: Room[]
|
|
2138
|
+
*
|
|
2139
|
+
* // Host.ts
|
|
2140
|
+
* @deco.HasMany('Room/Bedroom', { through: 'places', source: 'rooms' })
|
|
2141
|
+
* public bedrooms: Bedroom[]
|
|
2142
|
+
*
|
|
2143
|
+
* `Host.bedrooms` is the outermost association, so Bedroom's `dream:STI`
|
|
2144
|
+
* scope is applied once to the final rooms join. Bedroom is a subclass of
|
|
2145
|
+
* Room, so `Place.rooms` is a compatible path. Reversing those targets, or
|
|
2146
|
+
* mixing sibling STI children, raises instead of silently changing meaning.
|
|
2147
|
+
*/
|
|
2148
|
+
const modelClassWhoseDefaultScopesApplyToJoin = previousThroughAssociations[0]?.association.modelCB() ??
|
|
2149
|
+
polymorphicSourceTargetModelClass ??
|
|
2150
|
+
modelClassUsedForJoinConditions;
|
|
2109
2151
|
/**
|
|
2110
2152
|
* Stacked order/distinct clauses are applied in join order: the options of
|
|
2111
2153
|
* the through association whose bridging joins were added to the query
|
|
@@ -2131,22 +2173,22 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2131
2173
|
}
|
|
2132
2174
|
}
|
|
2133
2175
|
if (association.type === 'BelongsTo') {
|
|
2134
|
-
if (!
|
|
2176
|
+
if (!polymorphicSourceTargetModelClass && Array.isArray(association.modelCB()))
|
|
2135
2177
|
throw new CannotJoinPolymorphicBelongsToError({
|
|
2136
2178
|
dreamClass,
|
|
2137
2179
|
association,
|
|
2138
2180
|
innerJoinStatements: this.query['innerJoinStatements'],
|
|
2139
2181
|
leftJoinStatements: this.query['leftJoinStatements'],
|
|
2140
2182
|
});
|
|
2141
|
-
const to = (
|
|
2183
|
+
const to = (polymorphicSourceTargetModelClass ?? association.modelCB()).table;
|
|
2142
2184
|
const joinTableExpression = this.aliasedJoinTableExpression(to, currentTableAlias);
|
|
2143
2185
|
query = query[(joinType === 'inner' ? 'innerJoin' : 'leftJoin')](joinTableExpression, (join) => {
|
|
2144
2186
|
join = join.onRef(this.namespaceColumn(association.foreignKey(), previousTableAlias), '=', this.namespaceColumn(association.primaryKey(undefined, {
|
|
2145
|
-
associatedClassOverride:
|
|
2187
|
+
associatedClassOverride: polymorphicSourceTargetModelClass,
|
|
2146
2188
|
}), currentTableAlias));
|
|
2147
|
-
if (
|
|
2189
|
+
if (polymorphicSourceTargetModelClass) {
|
|
2148
2190
|
join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
|
|
2149
|
-
[association.foreignKeyTypeField()]:
|
|
2191
|
+
[association.foreignKeyTypeField()]: polymorphicSourceTargetModelClass.sanitizedName,
|
|
2150
2192
|
}, previousTableAlias)));
|
|
2151
2193
|
}
|
|
2152
2194
|
join = this.applyPreviousThroughAssociationAndStatementsToJoinStatement({
|
|
@@ -2156,14 +2198,14 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2156
2198
|
currentTableAlias,
|
|
2157
2199
|
joinAndStatement,
|
|
2158
2200
|
});
|
|
2159
|
-
join = this.
|
|
2201
|
+
join = this.applyAssociationDefaultScopesToJoin({
|
|
2160
2202
|
dreamClass,
|
|
2161
2203
|
join,
|
|
2162
2204
|
tableNameOrAlias: currentTableAlias,
|
|
2163
2205
|
association,
|
|
2164
|
-
|
|
2206
|
+
modelClassWhoseDefaultScopesApply: modelClassWhoseDefaultScopesApplyToJoin,
|
|
2165
2207
|
});
|
|
2166
|
-
join = this.applyJoinAndStatement(
|
|
2208
|
+
join = this.applyJoinAndStatement(modelClassUsedForJoinConditions, join, joinAndStatement, currentTableAlias);
|
|
2167
2209
|
return join;
|
|
2168
2210
|
});
|
|
2169
2211
|
}
|
|
@@ -2174,8 +2216,8 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2174
2216
|
join = join.onRef(this.namespaceColumn(association.primaryKey(), previousTableAlias), '=', this.namespaceColumn(association.foreignKey(), currentTableAlias));
|
|
2175
2217
|
if (association.polymorphic) {
|
|
2176
2218
|
join = join.on((eb) => this.whereStatementToExpressionWrapper(dreamClass, eb, this.aliasWhereStatement({
|
|
2177
|
-
[association.foreignKeyTypeField()]:
|
|
2178
|
-
?
|
|
2219
|
+
[association.foreignKeyTypeField()]: polymorphicSourceTargetModelClass
|
|
2220
|
+
? polymorphicSourceTargetModelClass.referenceTypeString
|
|
2179
2221
|
: dreamClass.referenceTypeString,
|
|
2180
2222
|
}, currentTableAlias)));
|
|
2181
2223
|
}
|
|
@@ -2194,14 +2236,14 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2194
2236
|
selfTableAlias,
|
|
2195
2237
|
joinAndStatement,
|
|
2196
2238
|
});
|
|
2197
|
-
join = this.
|
|
2239
|
+
join = this.applyAssociationDefaultScopesToJoin({
|
|
2198
2240
|
dreamClass,
|
|
2199
2241
|
join,
|
|
2200
2242
|
tableNameOrAlias: currentTableAlias,
|
|
2201
2243
|
association,
|
|
2202
|
-
|
|
2244
|
+
modelClassWhoseDefaultScopesApply: modelClassWhoseDefaultScopesApplyToJoin,
|
|
2203
2245
|
});
|
|
2204
|
-
join = this.applyJoinAndStatement(
|
|
2246
|
+
join = this.applyJoinAndStatement(modelClassUsedForJoinConditions, join, joinAndStatement, currentTableAlias);
|
|
2205
2247
|
return join;
|
|
2206
2248
|
});
|
|
2207
2249
|
if (association.type === 'HasMany') {
|
|
@@ -2312,11 +2354,9 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2312
2354
|
if (missingRequiredWhereStatements.length)
|
|
2313
2355
|
throw new MissingRequiredAssociationAndClause(association, missingRequiredWhereStatements[0]);
|
|
2314
2356
|
}
|
|
2315
|
-
|
|
2357
|
+
applyAssociationDefaultScopesToJoin({ dreamClass, join, tableNameOrAlias, association, modelClassWhoseDefaultScopesApply, }) {
|
|
2316
2358
|
let scopesQuery = new Query(this.dreamInstance);
|
|
2317
|
-
const
|
|
2318
|
-
const associationScopes = associationClass['scopes'].default;
|
|
2319
|
-
for (const scope of associationScopes) {
|
|
2359
|
+
for (const scope of modelClassWhoseDefaultScopesApply['scopes'].default) {
|
|
2320
2360
|
if (!shouldBypassDefaultScope(scope.method, {
|
|
2321
2361
|
bypassAllDefaultScopes: this.query['bypassAllDefaultScopes'],
|
|
2322
2362
|
defaultScopesToBypass: [
|
|
@@ -2324,7 +2364,7 @@ export default class KyselyQueryDriver extends QueryDriverBase {
|
|
|
2324
2364
|
...(association.withoutDefaultScopes || []),
|
|
2325
2365
|
],
|
|
2326
2366
|
})) {
|
|
2327
|
-
const tempQuery =
|
|
2367
|
+
const tempQuery = modelClassWhoseDefaultScopesApply[scope.method](scopesQuery);
|
|
2328
2368
|
// The scope method on a Dream model should return a clone of the Query it receives
|
|
2329
2369
|
// (e.g. by returning `scope.where(...)`), but in case the function doesn't return,
|
|
2330
2370
|
// or returns the wrong thing, we check before overriding `scopesQuery` with what the
|
|
@@ -5,6 +5,8 @@ export default async function loadedOrLoadAssociation(dream, dreamOrTransactionB
|
|
|
5
5
|
let scope = dreamOrTransactionBuilder.associationQuery(associationName, associationQueryOptions);
|
|
6
6
|
if (options?.passthrough)
|
|
7
7
|
scope = scope.passthrough(options.passthrough);
|
|
8
|
+
if (options?.connection)
|
|
9
|
+
scope = scope.connection(options.connection);
|
|
8
10
|
if (association?.type === 'HasMany') {
|
|
9
11
|
dream[associationName] = (await scope.all());
|
|
10
12
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installs one database-sourced scalar as the persisted value without
|
|
3
|
+
* replacing the saved-change history established by the current save.
|
|
4
|
+
*/
|
|
5
|
+
export default function installPersistedAttribute(dream, column, val) {
|
|
6
|
+
dream['currentAttributes'][column] = val;
|
|
7
|
+
dream['frozenAttributes'][column] = val;
|
|
8
|
+
dream['originalAttributes'][column] = val;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -3,6 +3,7 @@ import { clearSortableSaveState } from '../../decorators/field/sortable/helpers/
|
|
|
3
3
|
import performSortablePositionWork from '../../decorators/field/sortable/helpers/performSortablePositionWork.js';
|
|
4
4
|
import prepareSortableFieldsForSave from '../../decorators/field/sortable/helpers/prepareSortableFieldsForSave.js';
|
|
5
5
|
import { cacheSortableSnapshots } from '../../decorators/field/sortable/helpers/sortableSnapshot.js';
|
|
6
|
+
import CannotSaveMissingDream from '../../errors/CannotSaveMissingDream.js';
|
|
6
7
|
import ValidationError from '../../errors/ValidationError.js';
|
|
7
8
|
import sqlAttributes from '../../helpers/sqlAttributes.js';
|
|
8
9
|
import { DateTime } from '../../utils/datetime/DateTime.js';
|
|
@@ -114,6 +115,14 @@ async function writeDream(dream, txn, { alreadyPersisted, sortableFieldConfigs,
|
|
|
114
115
|
dream.createdAt = now;
|
|
115
116
|
if (!dream.dirtyAttributes().updatedAt && dream.columns().has('updatedAt'))
|
|
116
117
|
dream.updatedAt = now;
|
|
118
|
+
// The scope-lock read above positively found the row gone, so there is
|
|
119
|
+
// nothing to update. Raised here rather than after the write because the
|
|
120
|
+
// write below would not reach the database at all on a position-only save,
|
|
121
|
+
// whose attributes Sortable has already cleared; a save that does carry data
|
|
122
|
+
// raises the equivalent diagnostic from the driver, which sees the update
|
|
123
|
+
// match no row.
|
|
124
|
+
if (alreadyPersisted && !rowFoundBeforeWrite)
|
|
125
|
+
throw new CannotSaveMissingDream(dream);
|
|
117
126
|
const hasUnsavedData = !!Object.keys(sqlAttributes(dream)).length;
|
|
118
127
|
// BeforeSave/Update actions may clear all the data that we intended to save, leaving us with
|
|
119
128
|
// an invalid update command. The Sortable decorator is an example of this.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raised when saving a persisted instance whose database row no longer exists —
|
|
3
|
+
* the row was deleted between the moment the instance was loaded and the moment
|
|
4
|
+
* the save reached the database.
|
|
5
|
+
*
|
|
6
|
+
* This is the write-side counterpart of `RecordNotFound`, and applications catch
|
|
7
|
+
* it for the same reasons: a request updating a record another request has since
|
|
8
|
+
* deleted should answer 404 or 409 rather than 500, and a background job holding
|
|
9
|
+
* a stale instance should usually drop the work rather than fail loudly. Nothing
|
|
10
|
+
* is written when it is raised.
|
|
11
|
+
*
|
|
12
|
+
* It does not mean the save was rejected — a save that fails validation raises
|
|
13
|
+
* `ValidationError`, and a constraint violation raises its own error. It means
|
|
14
|
+
* the row that the instance stands for is gone.
|
|
15
|
+
*
|
|
16
|
+
* Dream raises it from the query driver, so a custom driver overriding
|
|
17
|
+
* `saveDream` translates its own adapter's empty-result error the same way, as
|
|
18
|
+
* the test-app MySQL driver does.
|
|
19
|
+
*/
|
|
20
|
+
export default class CannotSaveMissingDream extends Error {
|
|
21
|
+
constructor(dream) {
|
|
22
|
+
super(`Cannot save ${dream.sanitizedConstructorName} with primary key ${String(dream.primaryKeyValue())} because its persisted database row no longer exists.`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @internal
|
|
3
|
+
*
|
|
2
4
|
* Raised when a batch of a locked `Query#destroy` or `Query#update` on a
|
|
3
5
|
* sortable model would carry its transaction past the number of advisory
|
|
4
6
|
* sort-scope locks Dream is willing to hold at once.
|
|
@@ -27,6 +29,11 @@
|
|
|
27
29
|
* — a long multi-batch run inside a transaction you opened — either let Dream
|
|
28
30
|
* open a transaction per batch (do not pass a transaction of your own) or
|
|
29
31
|
* commit between runs, which is what releases the locks as it goes.
|
|
32
|
+
*
|
|
33
|
+
* Not exported from `@rvoh/dream/errors`. Exceeding the bound is a sizing
|
|
34
|
+
* mistake in the calling code, not a runtime condition an application can
|
|
35
|
+
* recover from, so there is nothing for a consumer to catch: the fix is a
|
|
36
|
+
* smaller batch or a narrower transaction, never a retry.
|
|
30
37
|
*/
|
|
31
38
|
export default class SortableBatchRequiresTooManyScopeLocks extends Error {
|
|
32
39
|
dreamClassName;
|