joist-core 2.3.0-next.98 → 2.3.0-next.99
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/build/BaseEntity.cjs.map +1 -1
- package/build/BaseEntity.js.map +1 -1
- package/build/Entity.cjs.map +1 -1
- package/build/Entity.js.map +1 -1
- package/build/EntityManager.cjs.map +1 -1
- package/build/EntityManager.d.cts.map +1 -1
- package/build/EntityManager.d.mts.map +1 -1
- package/build/EntityManager.js.map +1 -1
- package/build/EntityMetadata.cjs.map +1 -1
- package/build/EntityMetadata.js.map +1 -1
- package/build/IndexManager.cjs.map +1 -1
- package/build/IndexManager.js.map +1 -1
- package/build/InstanceData.cjs.map +1 -1
- package/build/InstanceData.js.map +1 -1
- package/build/PluginManager.cjs.map +1 -1
- package/build/PluginManager.js.map +1 -1
- package/build/batchloaders/enumCollectionBatchLoader.cjs.map +1 -1
- package/build/batchloaders/enumCollectionBatchLoader.js.map +1 -1
- package/build/batchloaders/loadBatchLoader.cjs.map +1 -1
- package/build/batchloaders/loadBatchLoader.js.map +1 -1
- package/build/batchloaders/manyToManyBatchLoader.cjs.map +1 -1
- package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
- package/build/batchloaders/oneToManyBatchLoader.cjs.map +1 -1
- package/build/batchloaders/oneToManyBatchLoader.js.map +1 -1
- package/build/batchloaders/oneToOneBatchLoader.cjs.map +1 -1
- package/build/batchloaders/oneToOneBatchLoader.js.map +1 -1
- package/build/batchloaders/populateBatchLoader.cjs.map +1 -1
- package/build/batchloaders/populateBatchLoader.js.map +1 -1
- package/build/batchloaders/recursiveChildrenBatchLoader.cjs.map +1 -1
- package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.cjs.map +1 -1
- package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
- package/build/batchloaders/recursiveParentsBatchLoader.cjs.map +1 -1
- package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
- package/build/changes.cjs.map +1 -1
- package/build/changes.js.map +1 -1
- package/build/config.cjs.map +1 -1
- package/build/config.js.map +1 -1
- package/build/configure.cjs.map +1 -1
- package/build/configure.js.map +1 -1
- package/build/dataloaders/fastWhereFilterHash.cjs.map +1 -1
- package/build/dataloaders/fastWhereFilterHash.js.map +1 -1
- package/build/dataloaders/findByUniqueDataLoader.cjs.map +1 -1
- package/build/dataloaders/findByUniqueDataLoader.d.cts.map +1 -1
- package/build/dataloaders/findByUniqueDataLoader.d.mts.map +1 -1
- package/build/dataloaders/findByUniqueDataLoader.js.map +1 -1
- package/build/dataloaders/findCountDataLoader.cjs.map +1 -1
- package/build/dataloaders/findCountDataLoader.js.map +1 -1
- package/build/dataloaders/findDataLoader.cjs +1 -1
- package/build/dataloaders/findDataLoader.cjs.map +1 -1
- package/build/dataloaders/findDataLoader.js +1 -1
- package/build/dataloaders/findDataLoader.js.map +1 -1
- package/build/dataloaders/findIdsDataLoader.cjs.map +1 -1
- package/build/dataloaders/findIdsDataLoader.js.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.cjs.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.cts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.d.mts.map +1 -1
- package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
- package/build/dataloaders/findPaginatedDataLoader.cjs.map +1 -1
- package/build/dataloaders/findPaginatedDataLoader.js.map +1 -1
- package/build/dataloaders/lensDataLoader.cjs.map +1 -1
- package/build/dataloaders/lensDataLoader.d.cts.map +1 -1
- package/build/dataloaders/lensDataLoader.d.mts.map +1 -1
- package/build/dataloaders/lensDataLoader.js.map +1 -1
- package/build/dataloaders/manyToManyFindDataLoader.cjs.map +1 -1
- package/build/dataloaders/manyToManyFindDataLoader.d.cts.map +1 -1
- package/build/dataloaders/manyToManyFindDataLoader.d.mts.map +1 -1
- package/build/dataloaders/manyToManyFindDataLoader.js.map +1 -1
- package/build/dataloaders/oneToManyFindDataLoader.cjs.map +1 -1
- package/build/dataloaders/oneToManyFindDataLoader.d.cts.map +1 -1
- package/build/dataloaders/oneToManyFindDataLoader.d.mts.map +1 -1
- package/build/dataloaders/oneToManyFindDataLoader.js.map +1 -1
- package/build/defaults.cjs.map +1 -1
- package/build/defaults.d.cts.map +1 -1
- package/build/defaults.d.mts.map +1 -1
- package/build/defaults.js.map +1 -1
- package/build/drivers/EntityWriter.cjs.map +1 -1
- package/build/drivers/EntityWriter.js.map +1 -1
- package/build/drivers/IdAssigner.cjs.map +1 -1
- package/build/drivers/IdAssigner.js.map +1 -1
- package/build/drivers/driverApi.cjs.map +1 -1
- package/build/drivers/driverApi.js.map +1 -1
- package/build/fields.cjs.map +1 -1
- package/build/fields.js.map +1 -1
- package/build/flush/JoinRows.cjs.map +1 -1
- package/build/flush/JoinRows.js.map +1 -1
- package/build/flush/Todo.cjs.map +1 -1
- package/build/flush/Todo.js.map +1 -1
- package/build/getProperties.cjs.map +1 -1
- package/build/getProperties.js.map +1 -1
- package/build/index.cjs +2 -2
- package/build/index.cjs.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/build/json.cjs.map +1 -1
- package/build/json.js.map +1 -1
- package/build/keys.cjs.map +1 -1
- package/build/keys.js.map +1 -1
- package/build/loading/HintTree.cjs.map +1 -1
- package/build/loading/HintTree.js.map +1 -1
- package/build/loading/IsLoadedCache.cjs.map +1 -1
- package/build/loading/IsLoadedCache.js.map +1 -1
- package/build/loading/loadHints.cjs.map +1 -1
- package/build/loading/loadHints.js.map +1 -1
- package/build/loading/loadLens.cjs.map +1 -1
- package/build/loading/loadLens.js.map +1 -1
- package/build/loading/withLoaded.cjs.map +1 -1
- package/build/loading/withLoaded.js.map +1 -1
- package/build/logging/FactoryLogger.cjs.map +1 -1
- package/build/logging/FactoryLogger.d.cts.map +1 -1
- package/build/logging/FactoryLogger.d.mts.map +1 -1
- package/build/logging/FactoryLogger.js.map +1 -1
- package/build/logging/FieldLogger.cjs.map +1 -1
- package/build/logging/FieldLogger.d.cts.map +1 -1
- package/build/logging/FieldLogger.d.mts.map +1 -1
- package/build/logging/FieldLogger.js.map +1 -1
- package/build/logging/ReactionLogger.cjs.map +1 -1
- package/build/logging/ReactionLogger.d.cts.map +1 -1
- package/build/logging/ReactionLogger.d.mts.map +1 -1
- package/build/logging/ReactionLogger.js.map +1 -1
- package/build/newEntity.cjs.map +1 -1
- package/build/newEntity.js.map +1 -1
- package/build/newTestInstance.cjs.map +1 -1
- package/build/newTestInstance.d.cts.map +1 -1
- package/build/newTestInstance.d.mts.map +1 -1
- package/build/newTestInstance.js.map +1 -1
- package/build/normalizeHints.cjs.map +1 -1
- package/build/normalizeHints.js.map +1 -1
- package/build/nowUTC.cjs.map +1 -1
- package/build/nowUTC.js.map +1 -1
- package/build/plugins/ImmutableEntitiesPlugin.cjs.map +1 -1
- package/build/plugins/ImmutableEntitiesPlugin.js.map +1 -1
- package/build/preloading/JsonAggregatePreloader.cjs.map +1 -1
- package/build/preloading/JsonAggregatePreloader.d.cts.map +1 -1
- package/build/preloading/JsonAggregatePreloader.d.mts.map +1 -1
- package/build/preloading/JsonAggregatePreloader.js.map +1 -1
- package/build/preloading/canPreload.cjs.map +1 -1
- package/build/preloading/canPreload.js.map +1 -1
- package/build/preloading/partitionHint.cjs.map +1 -1
- package/build/preloading/partitionHint.js.map +1 -1
- package/build/queries/ConditionBuilder.cjs.map +1 -1
- package/build/queries/ConditionBuilder.js.map +1 -1
- package/build/queries/conditions.cjs.map +1 -1
- package/build/queries/conditions.js.map +1 -1
- package/build/queries/entityQueryUtils.cjs.map +1 -1
- package/build/queries/entityQueryUtils.js.map +1 -1
- package/build/queries/find/Aliases.cjs.map +1 -1
- package/build/queries/find/Aliases.d.cts.map +1 -1
- package/build/queries/find/Aliases.d.mts.map +1 -1
- package/build/queries/find/Aliases.js.map +1 -1
- package/build/queries/find/DeferredAlias.cjs.map +1 -1
- package/build/queries/find/DeferredAlias.js.map +1 -1
- package/build/queries/find/EntityFilter.cjs.map +1 -1
- package/build/queries/find/EntityFilter.js.map +1 -1
- package/build/queries/find/EntityGraphQLFilter.cjs.map +1 -1
- package/build/queries/find/EntityGraphQLFilter.js.map +1 -1
- package/build/queries/find/QueryParser.cjs +1 -1
- package/build/queries/find/QueryParser.cjs.map +1 -1
- package/build/queries/find/QueryParser.collectionJoins.cjs.map +1 -1
- package/build/queries/find/QueryParser.collectionJoins.js.map +1 -1
- package/build/queries/find/QueryParser.d.cts.map +1 -1
- package/build/queries/find/QueryParser.d.mts.map +1 -1
- package/build/queries/find/QueryParser.js +1 -1
- package/build/queries/find/QueryParser.js.map +1 -1
- package/build/queries/find/QueryParser.pruning.cjs.map +1 -1
- package/build/queries/find/QueryParser.pruning.js.map +1 -1
- package/build/queries/find/QueryVisitor.cjs.map +1 -1
- package/build/queries/find/QueryVisitor.js.map +1 -1
- package/build/queries/find/buildFindQuery.cjs.map +1 -1
- package/build/queries/find/buildFindQuery.js.map +1 -1
- package/build/queries/find/scopes.cjs.map +1 -1
- package/build/queries/find/scopes.js.map +1 -1
- package/build/queries/parsedConditions.cjs.map +1 -1
- package/build/queries/parsedConditions.js.map +1 -1
- package/build/queries/renderConditions.cjs.map +1 -1
- package/build/queries/renderConditions.js.map +1 -1
- package/build/queries/skipCondition.cjs.map +1 -1
- package/build/queries/skipCondition.js.map +1 -1
- package/build/queries/sql/AliasAssigner.cjs.map +1 -1
- package/build/queries/sql/AliasAssigner.js.map +1 -1
- package/build/queries/sql/Expr.cjs.map +1 -1
- package/build/queries/sql/Expr.js.map +1 -1
- package/build/queries/sql/Tables.cjs.map +1 -1
- package/build/queries/sql/Tables.d.cts.map +1 -1
- package/build/queries/sql/Tables.d.mts.map +1 -1
- package/build/queries/sql/Tables.js.map +1 -1
- package/build/queries/sql/execute.cjs.map +1 -1
- package/build/queries/sql/execute.js.map +1 -1
- package/build/queries/sql/expressions/arrayAgg.cjs.map +1 -1
- package/build/queries/sql/expressions/arrayAgg.js.map +1 -1
- package/build/queries/sql/expressions/case.cjs.map +1 -1
- package/build/queries/sql/expressions/case.js.map +1 -1
- package/build/queries/sql/expressions/coalesce.cjs.map +1 -1
- package/build/queries/sql/expressions/coalesce.js.map +1 -1
- package/build/queries/sql/expressions/codecs.cjs.map +1 -1
- package/build/queries/sql/expressions/codecs.js.map +1 -1
- package/build/queries/sql/expressions/expression.cjs.map +1 -1
- package/build/queries/sql/expressions/expression.d.cts.map +1 -1
- package/build/queries/sql/expressions/expression.d.mts.map +1 -1
- package/build/queries/sql/expressions/expression.js.map +1 -1
- package/build/queries/sql/expressions/greatest.cjs.map +1 -1
- package/build/queries/sql/expressions/greatest.js.map +1 -1
- package/build/queries/sql/expressions/least.cjs.map +1 -1
- package/build/queries/sql/expressions/least.js.map +1 -1
- package/build/queries/sql/expressions/nullIf.cjs.map +1 -1
- package/build/queries/sql/expressions/nullIf.js.map +1 -1
- package/build/queries/sql/expressions/parseExpression.cjs.map +1 -1
- package/build/queries/sql/expressions/parseExpression.js.map +1 -1
- package/build/queries/sql/expressions/types.d.cts.map +1 -1
- package/build/queries/sql/expressions/types.d.mts.map +1 -1
- package/build/queries/sql/query.cjs +1 -1
- package/build/queries/sql/query.cjs.map +1 -1
- package/build/queries/sql/query.js +1 -1
- package/build/queries/sql/query.js.map +1 -1
- package/build/queries/unnest.cjs.map +1 -1
- package/build/queries/unnest.js.map +1 -1
- package/build/queries/valueFilters.cjs.map +1 -1
- package/build/queries/valueFilters.d.cts.map +1 -1
- package/build/queries/valueFilters.d.mts.map +1 -1
- package/build/queries/valueFilters.js.map +1 -1
- package/build/reactivity/ReactionsManager.cjs.map +1 -1
- package/build/reactivity/ReactionsManager.js.map +1 -1
- package/build/reactivity/reactiveHints.cjs.map +1 -1
- package/build/reactivity/reactiveHints.js.map +1 -1
- package/build/relations/AbstractRelationImpl.cjs.map +1 -1
- package/build/relations/AbstractRelationImpl.js.map +1 -1
- package/build/relations/AsyncProperty.cjs.map +1 -1
- package/build/relations/AsyncProperty.js.map +1 -1
- package/build/relations/AsyncReactiveField.cjs.map +1 -1
- package/build/relations/AsyncReactiveField.js.map +1 -1
- package/build/relations/Collection.cjs.map +1 -1
- package/build/relations/Collection.js.map +1 -1
- package/build/relations/CustomCollection.cjs.map +1 -1
- package/build/relations/CustomCollection.js.map +1 -1
- package/build/relations/CustomReference.cjs.map +1 -1
- package/build/relations/CustomReference.js.map +1 -1
- package/build/relations/EnumCollection.cjs.map +1 -1
- package/build/relations/EnumCollection.js.map +1 -1
- package/build/relations/LazyField.cjs.map +1 -1
- package/build/relations/LazyField.js.map +1 -1
- package/build/relations/ManyToManyCollection.cjs.map +1 -1
- package/build/relations/ManyToManyCollection.js.map +1 -1
- package/build/relations/ManyToManyLargeCollection.cjs.map +1 -1
- package/build/relations/ManyToManyLargeCollection.js.map +1 -1
- package/build/relations/ManyToOneReference.cjs.map +1 -1
- package/build/relations/ManyToOneReference.js.map +1 -1
- package/build/relations/OneToManyCollection.cjs.map +1 -1
- package/build/relations/OneToManyCollection.js.map +1 -1
- package/build/relations/OneToManyLargeCollection.cjs.map +1 -1
- package/build/relations/OneToManyLargeCollection.js.map +1 -1
- package/build/relations/OneToOneReference.cjs.map +1 -1
- package/build/relations/OneToOneReference.js.map +1 -1
- package/build/relations/PolymorphicReference.cjs.map +1 -1
- package/build/relations/PolymorphicReference.js.map +1 -1
- package/build/relations/ReactiveField.cjs.map +1 -1
- package/build/relations/ReactiveField.js.map +1 -1
- package/build/relations/ReactiveGetter.cjs.map +1 -1
- package/build/relations/ReactiveGetter.js.map +1 -1
- package/build/relations/ReactiveManyToMany.cjs.map +1 -1
- package/build/relations/ReactiveManyToMany.js.map +1 -1
- package/build/relations/ReactiveManyToManyOtherSide.cjs.map +1 -1
- package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
- package/build/relations/ReactiveReference.cjs.map +1 -1
- package/build/relations/ReactiveReference.js.map +1 -1
- package/build/relations/ReadOnlyCollection.cjs.map +1 -1
- package/build/relations/ReadOnlyCollection.js.map +1 -1
- package/build/relations/RecursiveCollection.cjs.map +1 -1
- package/build/relations/RecursiveCollection.js.map +1 -1
- package/build/relations/RecursiveCycleError.cjs.map +1 -1
- package/build/relations/RecursiveCycleError.js.map +1 -1
- package/build/relations/Reference.cjs.map +1 -1
- package/build/relations/Reference.js.map +1 -1
- package/build/relations/Relation.cjs.map +1 -1
- package/build/relations/Relation.js.map +1 -1
- package/build/relations/hasAsyncMethod.cjs.map +1 -1
- package/build/relations/hasAsyncMethod.js.map +1 -1
- package/build/relations/hasManyDerived.cjs.map +1 -1
- package/build/relations/hasManyDerived.js.map +1 -1
- package/build/relations/hasManyThrough.cjs.map +1 -1
- package/build/relations/hasManyThrough.js.map +1 -1
- package/build/relations/hasOneDerived.cjs.map +1 -1
- package/build/relations/hasOneDerived.js.map +1 -1
- package/build/relations/hasOneThrough.cjs.map +1 -1
- package/build/relations/hasOneThrough.js.map +1 -1
- package/build/relations/hasProperty.cjs.map +1 -1
- package/build/relations/hasProperty.js.map +1 -1
- package/build/relations/isCascadeDelete.cjs.map +1 -1
- package/build/relations/isCascadeDelete.js.map +1 -1
- package/build/resurrection.cjs.map +1 -1
- package/build/resurrection.js.map +1 -1
- package/build/rules.cjs.map +1 -1
- package/build/rules.js.map +1 -1
- package/build/runtimeConfig.cjs.map +1 -1
- package/build/runtimeConfig.js.map +1 -1
- package/build/serde/TypeInfo.cjs.map +1 -1
- package/build/serde/TypeInfo.js.map +1 -1
- package/build/serde/columns.cjs.map +1 -1
- package/build/serde/columns.js.map +1 -1
- package/build/serde/fieldSerde.cjs.map +1 -1
- package/build/serde/fieldSerde.js.map +1 -1
- package/build/serde/serde.cjs.map +1 -1
- package/build/serde/serde.d.cts.map +1 -1
- package/build/serde/serde.d.mts.map +1 -1
- package/build/serde/serde.js.map +1 -1
- package/build/serde/temporalMappers.cjs.map +1 -1
- package/build/serde/temporalMappers.js.map +1 -1
- package/build/upsert.cjs.map +1 -1
- package/build/upsert.d.cts.map +1 -1
- package/build/upsert.d.mts.map +1 -1
- package/build/upsert.js.map +1 -1
- package/build/utils.cjs.map +1 -1
- package/build/utils.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unnest.cjs","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"
|
|
1
|
+
{"version":3,"file":"unnest.cjs","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\nfunction notNullMarker(c: OpColumn): any {\n // Should probably push this into an OpColumn.notNullMarker value\n if (c.dbType.startsWith(\"varchar\") || c.dbType.startsWith(\"character\") || c.dbType.startsWith(\"text\")) {\n return \"\";\n } else if (c.dbType.startsWith(\"int\")) {\n return 1;\n } else if (c.dbType.startsWith(\"date\")) {\n return new Date(); // maybe something temporal?\n } else if (c.dbType.startsWith(\"timestamp\")) {\n return new Date(); // maybe something temporal?\n } else {\n throw new Error(\"Unhandled nullable array column type \" + c.dbType);\n }\n}\n"],"mappings":";;;;;;;;;;AAWA,SAAgB,eAAe,OAAe,SAAqB,cAAwC;CAEzG,4BAA4B,SAAS,YAAY;CAajD,OAAO;EACL;EACA;EACA,OAAO;GAAE,MAAM;GAAO,KAAK,UAdb,QAAQ,KAAK,MAAM;IACjC,IAAI,EAAE,OAAO,SAAS,IAAI,GAAG;KAC3B,IAAI,EAAE,iBACJ,OAAO,oBAAoB,EAAE,OAAO;UAEpC,OAAO,oBAAoB,EAAE,OAAO;IAExC,OACE,OAAO,aAAa,EAAE,OAAO;GAEjC,CAI6C,CAAC,CAAC,KAAK,IAAI;GAAK,UAAU;EAAa;CACpF;AACF;AAEA,SAAgB,4BAA4B,SAAqB,cAA6B;CAC5F,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,OAAO,SAAS,IAAI,GAAG;GAChC,MAAM,SAAS,aAAa;GAC5B,MAAM,SAAS,iBAAiB,MAAM;GACtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,OAAO,KAAK,mBAAmB,QAAQ,OAAO,IAAI,MAAM;EAE5D;CACF;AACF;AAEA,SAAS,iBAAiB,cAAwC;CAChE,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,aAAa,EAAE,EAAE,UAAU,CAAC;CAC9F,OAAO;AACT;AAIA,SAAS,mBAAmB,GAAa,OAAqB,SAAwB;CACpF,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;CACrC,MAAM,aAAa,KAAK,IAAI,GAAG,WAAW,OAAO,UAAU,EAAE;CAC7D,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,OAAO,KAAK,IAAI;CAErD,IAAI,EAAE,iBAAiB,OAAO,QAAQ,UAAU,OAAO,cAAc,CAAC,CAAC;CACvE,OAAO;AACT;AAEA,SAAS,cAAc,GAAkB;CAEvC,IAAI,EAAE,OAAO,WAAW,SAAS,KAAK,EAAE,OAAO,WAAW,WAAW,KAAK,EAAE,OAAO,WAAW,MAAM,GAClG,OAAO;MACF,IAAI,EAAE,OAAO,WAAW,KAAK,GAClC,OAAO;MACF,IAAI,EAAE,OAAO,WAAW,MAAM,GACnC,uBAAO,IAAI,KAAK;MACX,IAAI,EAAE,OAAO,WAAW,WAAW,GACxC,uBAAO,IAAI,KAAK;MAEhB,MAAM,IAAI,MAAM,0CAA0C,EAAE,MAAM;AAEtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unnest.js","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"
|
|
1
|
+
{"version":3,"file":"unnest.js","names":[],"sources":["../../src/queries/unnest.ts"],"sourcesContent":["import type { OpColumn } from \"src/drivers/EntityWriter.ts\";\nimport type { ParsedCteClause } from \"src/queries/find/QueryParser.ts\";\n\n/**\n * Creates a CTE named `alias` that bulk-injects the `columnValues` into a SQL query.\n *\n * I.e. `columns = [first_name, last_name]` means columnValues would be all first names,\n * then all last names, i.e.`[[\"f1\",\"f2\"], [\"l1\",\"l2\"]]`.\n *\n * Note that `columnValues` will be mutated if any array columns need to be made rectangular.\n */\nexport function buildUnnestCte(alias: string, columns: OpColumn[], columnValues: any[][]): ParsedCteClause {\n // Make any arrays-of-arrays rectangular by filling them with nulls\n ensureRectangularArraySizes(columns, columnValues);\n // Use unnest or unnest_arrays depending on whether the column is an array column\n const selects = columns.map((c) => {\n if (c.dbType.endsWith(\"[]\")) {\n if (c.isNullableArray) {\n return `unnest_arrays(?::${c.dbType}[], true)`;\n } else {\n return `unnest_arrays(?::${c.dbType}[])`;\n }\n } else {\n return `unnest(?::${c.dbType}[])`;\n }\n });\n return {\n alias,\n columns,\n query: { kind: \"raw\", sql: `SELECT ${selects.join(\", \")}`, bindings: columnValues },\n };\n}\n\nexport function ensureRectangularArraySizes(columns: OpColumn[], columnValues: any[][]): void {\n for (let i = 0; i < columns.length; i++) {\n const column = columns[i];\n if (column.dbType.endsWith(\"[]\")) {\n const values = columnValues[i];\n const fillTo = findArrayMaxSize(values);\n for (let j = 0; j < values.length; j++) {\n values[j] = fillArrayWithNulls(column, values[j], fillTo);\n }\n }\n }\n}\n\nfunction findArrayMaxSize(columnValues: readonly any[][]): number {\n let max = 1; // postgres really doesn't like `{{}}` so always send at least 1 element\n for (let i = 0; i < columnValues.length; i++) max = Math.max(max, columnValues[i]?.length ?? 0);\n return max;\n}\n\n// Because postgres array-of-arrays must be rectangular, we fill all arrays up the same max size\n// to put on the wire, and then later strip the padded nulls during the unnest_arrays call.\nfunction fillArrayWithNulls(c: OpColumn, array: any[] | null, maxSize: number): any[] {\n const wasNull = array === null;\n const result = array ? [...array] : [];\n const nullsToAdd = Math.max(0, maxSize - (array?.length ?? 0));\n for (let i = 0; i < nullsToAdd; i++) result.push(null);\n // Add our unset marker\n if (c.isNullableArray) result.unshift(wasNull ? null : notNullMarker(c));\n return result;\n}\n\nfunction notNullMarker(c: OpColumn): any {\n // Should probably push this into an OpColumn.notNullMarker value\n if (c.dbType.startsWith(\"varchar\") || c.dbType.startsWith(\"character\") || c.dbType.startsWith(\"text\")) {\n return \"\";\n } else if (c.dbType.startsWith(\"int\")) {\n return 1;\n } else if (c.dbType.startsWith(\"date\")) {\n return new Date(); // maybe something temporal?\n } else if (c.dbType.startsWith(\"timestamp\")) {\n return new Date(); // maybe something temporal?\n } else {\n throw new Error(\"Unhandled nullable array column type \" + c.dbType);\n }\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,eAAe,OAAe,SAAqB,cAAwC;CAEzG,4BAA4B,SAAS,YAAY;CAajD,OAAO;EACL;EACA;EACA,OAAO;GAAE,MAAM;GAAO,KAAK,UAdb,QAAQ,KAAK,MAAM;IACjC,IAAI,EAAE,OAAO,SAAS,IAAI,GAAG;KAC3B,IAAI,EAAE,iBACJ,OAAO,oBAAoB,EAAE,OAAO;UAEpC,OAAO,oBAAoB,EAAE,OAAO;IAExC,OACE,OAAO,aAAa,EAAE,OAAO;GAEjC,CAI6C,CAAC,CAAC,KAAK,IAAI;GAAK,UAAU;EAAa;CACpF;AACF;AAEA,SAAgB,4BAA4B,SAAqB,cAA6B;CAC5F,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;EACvB,IAAI,OAAO,OAAO,SAAS,IAAI,GAAG;GAChC,MAAM,SAAS,aAAa;GAC5B,MAAM,SAAS,iBAAiB,MAAM;GACtC,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KACjC,OAAO,KAAK,mBAAmB,QAAQ,OAAO,IAAI,MAAM;EAE5D;CACF;AACF;AAEA,SAAS,iBAAiB,cAAwC;CAChE,IAAI,MAAM;CACV,KAAK,IAAI,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK,MAAM,KAAK,IAAI,KAAK,aAAa,EAAE,EAAE,UAAU,CAAC;CAC9F,OAAO;AACT;AAIA,SAAS,mBAAmB,GAAa,OAAqB,SAAwB;CACpF,MAAM,UAAU,UAAU;CAC1B,MAAM,SAAS,QAAQ,CAAC,GAAG,KAAK,IAAI,CAAC;CACrC,MAAM,aAAa,KAAK,IAAI,GAAG,WAAW,OAAO,UAAU,EAAE;CAC7D,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,KAAK,OAAO,KAAK,IAAI;CAErD,IAAI,EAAE,iBAAiB,OAAO,QAAQ,UAAU,OAAO,cAAc,CAAC,CAAC;CACvE,OAAO;AACT;AAEA,SAAS,cAAc,GAAkB;CAEvC,IAAI,EAAE,OAAO,WAAW,SAAS,KAAK,EAAE,OAAO,WAAW,WAAW,KAAK,EAAE,OAAO,WAAW,MAAM,GAClG,OAAO;MACF,IAAI,EAAE,OAAO,WAAW,KAAK,GAClC,OAAO;MACF,IAAI,EAAE,OAAO,WAAW,MAAM,GACnC,uBAAO,IAAI,KAAK;MACX,IAAI,EAAE,OAAO,WAAW,WAAW,GACxC,uBAAO,IAAI,KAAK;MAEhB,MAAM,IAAI,MAAM,0CAA0C,EAAE,MAAM;AAEtE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valueFilters.cjs","names":["isAlias","isScope","isEntity","isPlainObject","isDefined","assertNever"],"sources":["../../src/queries/valueFilters.ts"],"sourcesContent":["import { isPlainObject } from \"joist-utils\";\n\nimport { type Entity, isEntity } from \"../Entity.ts\";\nimport { isDefined } from \"../EntityManager.ts\";\nimport type { EntityMetadata } from \"../EntityMetadata.ts\";\nimport type { Column } from \"../serde/columns.ts\";\nimport { assertNever } from \"../utils.ts\";\nimport { isAlias } from \"./find/Aliases.ts\";\nimport type { ValueFilter } from \"./find/EntityFilter.ts\";\nimport { isScope } from \"./find/scopes.ts\";\nimport type { ParsedValueFilter } from \"./parsedConditions.ts\";\n\n/** An ADT version of `EntityFilter`. */\nexport type ParsedEntityFilter =\n // ParsedValueFilter is any simple match on `id`\n | ParsedValueFilter<string | number>\n // Otherwise we return the join/complex\n | { kind: \"join\"; subFilter: object };\n\n/** Parses an entity filter, which could be \"just an id\", an array of ids, or a nested filter. */\nexport function parseEntityFilter(meta: EntityMetadata, filter: any): ParsedEntityFilter | undefined {\n if (filter === undefined) {\n // This matches legacy `em.find(Book, { author: undefined })` behavior\n return undefined;\n } else if (isAlias(filter)) {\n // We're just binding an alias to this position in the join tree\n return undefined;\n } else if (isScope(filter)) {\n // A scope on a relation (e.g. `{ author: Author.adult }`) always implies conditions on the\n // related entity, so signal \"needs a join\" via the existing `join` discriminant rather than\n // adding a dedicated `kind: \"scope\"` that every relation branch would have to learn. The empty\n // `subFilter` is intentionally never read: the m2o/m2m branches only check `kind === \"join\"` to\n // decide whether to join, then re-dispatch the original scope value back through `addTable`,\n // where `isScope(...)` routes it to `addScopeFilterAt` to apply the scope's actual fragments.\n return { kind: \"join\", subFilter: {} };\n } else if (filter === null) {\n return { kind: \"is-null\" };\n } else if (typeof filter === \"string\" || typeof filter === \"number\") {\n return { kind: \"eq\", value: filter };\n } else if (typeof filter === \"boolean\") {\n return filter ? { kind: \"not-null\" } : { kind: \"is-null\" };\n } else if (Array.isArray(filter)) {\n return {\n kind: \"in\",\n value: filter.map((v: string | number | Entity) => {\n return isEntity(v) ? (v.idTaggedMaybe ?? nilIdValue(meta)) : v;\n }),\n };\n } else if (isEntity(filter)) {\n return { kind: \"eq\", value: filter.idTaggedMaybe || nilIdValue(meta) };\n } else if (typeof filter === \"object\") {\n // Looking for `{ firstName: \"f1\" }` or `{ ne: \"f1\" }`\n const keys = Object.keys(filter);\n // Special case only looking at `ne`\n if (keys.length === 1 && keys[0] === \"ne\") {\n const value = filter[\"ne\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"not-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"ne\", value };\n } else if (isEntity(value)) {\n return { kind: \"ne\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n throw new Error(`Unsupported \"ne\" value ${value}`);\n }\n }\n // Special case only looking at `id`\n if (keys.length === 1 && keys[0] === \"id\") {\n const value = filter[\"id\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"is-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"eq\", value };\n } else if (isEntity(value)) {\n return { kind: \"eq\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n return parseValueFilter(value)[0] as any;\n }\n }\n // Look for subFilter values being EntityFilter-ish instances like ManyToOneReference\n // that have an id, and so structurally match the entity filter without really being filters,\n // and convert them over here before getting into parseValueFilter.\n const subFilter = {} as any;\n for (const [key, value] of Object.entries(filter)) {\n if (value && typeof value === \"object\" && !isPlainObject(value) && \"idTaggedMaybe\" in value) {\n subFilter[key] = value.idTaggedMaybe || nilIdValue(meta);\n } else {\n subFilter[key] = value;\n }\n }\n return { kind: \"join\", subFilter };\n } else {\n throw new Error(`Unrecognized filter ${filter}`);\n }\n}\n\n/**\n * Parses the many/hodgepodge (ergonomic!) patterns of value filters into a `ParsedValueFilter[]`.\n *\n * Note we return an array because filter might be a `ValueGraphQLFilter` that is allowed to have\n * multiple conditions, i.e. `{ lt: 10, gt: 5 }`.\n */\nexport function parseValueFilter<V>(filter: ValueFilter<V, any>): ParsedValueFilter<V>[] {\n if (filter === null) {\n return [{ kind: \"is-null\" }];\n } else if (filter === undefined) {\n // This is legacy behavior where `em.find(Book, { author: undefined })` would match all books\n return [];\n } else if (Array.isArray(filter)) {\n return [{ kind: \"in\", value: filter }];\n } else if (isPlainObject(filter)) {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n // Should this be an error?\n return [];\n } else if (keys.length === 2 && \"op\" in filter && \"value\" in filter) {\n // Probe for `findGql` op & value\n const { op, value } = filter;\n if (shouldPruneValueFilter(op, value)) {\n return [];\n } else if (value === null) {\n return [{ kind: \"is-null\" }];\n } else {\n return [{ kind: op, value: value ?? null }];\n }\n } else if (keys.length === 2 && \"gte\" in filter && \"lte\" in filter) {\n const { gte, lte } = filter;\n return [{ kind: \"between\", value: [gte, lte] }];\n } else {\n return Object.entries(filter)\n .map(([key, value]) => {\n // Always do condition pruning on the value\n if (shouldPruneValueFilter(key, value)) {\n return undefined;\n }\n switch (key) {\n case \"eq\":\n if (value === null) {\n return { kind: \"is-null\" as const };\n } else {\n return { kind: \"eq\" as const, value: filter[key] };\n }\n case \"ne\":\n if (value === null) {\n return { kind: \"not-null\" as const };\n } else {\n return { kind: \"ne\" as const, value: filter[key] ?? null };\n }\n case \"in\":\n case \"nin\":\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\":\n return { kind: key, value: filter[key] };\n case \"pathExists\":\n return { kind: \"jsonPathExists\" as const, value: filter[key] };\n case \"pathIsTrue\":\n return { kind: \"jsonPathPredicate\" as const, value: filter[key] };\n case \"search\":\n return { kind: \"ilike\" as const, value: makeLike(filter[key]) };\n case \"between\":\n return { kind: key, value: filter[key] };\n default:\n throw new Error(`Unsupported value filter key ${key}`);\n }\n })\n .filter(isDefined);\n }\n } else {\n // This is a primitive like a string, number\n return [{ kind: \"eq\", value: filter }];\n }\n}\n\n/** Converts domain-level values like string ids/enums into their db equivalent. */\nexport function mapToDb(column: Column, filter: ParsedValueFilter<any>): ParsedValueFilter<any> {\n // ...to teach this `mapToDb` function to handle/rewrite `in: [1, null]` handling, we'd need to:\n // 1. return a maybe-simple/maybe-nested condition, so basically a `ParsedExpressionCondition`, because\n // this would let `in` return an `{ or: ... }` to all the callers.\n // 2. also return `{ parsed: ParsedExpressionCondition, simples: SimpleCondition[] }` tuple, for the\n // alias `addCondition` processing to track the `simples` and rewrite their alias when later bound.\n switch (filter.kind) {\n case \"eq\":\n case \"gt\":\n case \"gte\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n filter.value = column.mapToDb(filter.value);\n return filter;\n case \"in\":\n if (column.isArray) {\n // Arrays need a special operator\n return { kind: \"contains\", value: column.mapToDb(filter.value) };\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"nin\":\n if (column.isArray) {\n // Arrays need a special operator\n throw new Error(\"The nin operator is not supported on array columns yet\");\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"contains\":\n case \"ncontains\":\n case \"overlaps\":\n case \"noverlaps\":\n case \"containedBy\":\n const supportsContains = column.isArray || column.dbType === \"jsonb\";\n if (!supportsContains) {\n throw new Error(`${filter.kind} is only unsupported on array or jsonb columns`);\n }\n if (column.isArray) {\n filter.value = column.mapToDb(filter.value);\n } else {\n // leave jsonb values alone\n }\n return filter;\n case \"between\":\n filter.value = [column.mapToDb(filter.value[0]), column.mapToDb(filter.value[1])];\n return filter;\n case \"is-null\":\n case \"not-null\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n return filter;\n default:\n throw assertNever(filter);\n }\n}\n\n/** Converts a search term like `foo bar` into a SQL `like` pattern like `%foo%bar%`. */\nexport function makeLike(search: any | undefined): any {\n return search ? `%${search.replace(/\\s+/g, \"%\")}%` : undefined;\n}\n\n/** Recognizes placeholder IDs used when a filter contains an unpersisted entity. */\nexport function isNilIdValue(value: unknown): boolean {\n return value === -1 || value === \"00000000-0000-0000-0000-000000000000\";\n}\n\n/**\n * We use this value if users include new (id-less) entities as em.find conditions.\n *\n * The idea is that this condition would never be met, but we still want to do the em.find\n * query in case it's in an `OR` clause that would match false, but some other part of the\n * clause would match. I.e. instead of just skipping the DB query all together, which is\n * also something we could consider doing.\n *\n * For int IDs we use -1, and for uuid IDs, we use the nil UUID value:\n *\n * https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID\n */\nfunction nilIdValue(meta: EntityMetadata): any {\n switch (meta.idDbType) {\n case \"int\":\n case \"bigint\":\n return -1;\n case \"uuid\":\n return \"00000000-0000-0000-0000-000000000000\";\n case \"text\":\n return \"0\";\n default:\n return assertNever(meta.idDbType);\n }\n}\n\n/** Returns true for values that should be treated like an omitted filter. */\nfunction shouldPruneValueFilter(key: unknown, value: unknown): boolean {\n if (value === undefined) return true;\n return value === false && key !== \"eq\" && key !== \"ne\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"valueFilters.cjs","names":["isAlias","isScope","isEntity","isPlainObject","isDefined","assertNever"],"sources":["../../src/queries/valueFilters.ts"],"sourcesContent":["import { isPlainObject } from \"joist-utils\";\nimport { type Entity, isEntity } from \"src/Entity.ts\";\nimport { isDefined } from \"src/EntityManager.ts\";\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport { isAlias } from \"src/queries/find/Aliases.ts\";\nimport type { ValueFilter } from \"src/queries/find/EntityFilter.ts\";\nimport { isScope } from \"src/queries/find/scopes.ts\";\nimport type { ParsedValueFilter } from \"src/queries/parsedConditions.ts\";\nimport type { Column } from \"src/serde/columns.ts\";\nimport { assertNever } from \"src/utils.ts\";\n\n/** An ADT version of `EntityFilter`. */\nexport type ParsedEntityFilter =\n // ParsedValueFilter is any simple match on `id`\n | ParsedValueFilter<string | number>\n // Otherwise we return the join/complex\n | { kind: \"join\"; subFilter: object };\n\n/** Parses an entity filter, which could be \"just an id\", an array of ids, or a nested filter. */\nexport function parseEntityFilter(meta: EntityMetadata, filter: any): ParsedEntityFilter | undefined {\n if (filter === undefined) {\n // This matches legacy `em.find(Book, { author: undefined })` behavior\n return undefined;\n } else if (isAlias(filter)) {\n // We're just binding an alias to this position in the join tree\n return undefined;\n } else if (isScope(filter)) {\n // A scope on a relation (e.g. `{ author: Author.adult }`) always implies conditions on the\n // related entity, so signal \"needs a join\" via the existing `join` discriminant rather than\n // adding a dedicated `kind: \"scope\"` that every relation branch would have to learn. The empty\n // `subFilter` is intentionally never read: the m2o/m2m branches only check `kind === \"join\"` to\n // decide whether to join, then re-dispatch the original scope value back through `addTable`,\n // where `isScope(...)` routes it to `addScopeFilterAt` to apply the scope's actual fragments.\n return { kind: \"join\", subFilter: {} };\n } else if (filter === null) {\n return { kind: \"is-null\" };\n } else if (typeof filter === \"string\" || typeof filter === \"number\") {\n return { kind: \"eq\", value: filter };\n } else if (typeof filter === \"boolean\") {\n return filter ? { kind: \"not-null\" } : { kind: \"is-null\" };\n } else if (Array.isArray(filter)) {\n return {\n kind: \"in\",\n value: filter.map((v: string | number | Entity) => {\n return isEntity(v) ? (v.idTaggedMaybe ?? nilIdValue(meta)) : v;\n }),\n };\n } else if (isEntity(filter)) {\n return { kind: \"eq\", value: filter.idTaggedMaybe || nilIdValue(meta) };\n } else if (typeof filter === \"object\") {\n // Looking for `{ firstName: \"f1\" }` or `{ ne: \"f1\" }`\n const keys = Object.keys(filter);\n // Special case only looking at `ne`\n if (keys.length === 1 && keys[0] === \"ne\") {\n const value = filter[\"ne\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"not-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"ne\", value };\n } else if (isEntity(value)) {\n return { kind: \"ne\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n throw new Error(`Unsupported \"ne\" value ${value}`);\n }\n }\n // Special case only looking at `id`\n if (keys.length === 1 && keys[0] === \"id\") {\n const value = filter[\"id\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"is-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"eq\", value };\n } else if (isEntity(value)) {\n return { kind: \"eq\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n return parseValueFilter(value)[0] as any;\n }\n }\n // Look for subFilter values being EntityFilter-ish instances like ManyToOneReference\n // that have an id, and so structurally match the entity filter without really being filters,\n // and convert them over here before getting into parseValueFilter.\n const subFilter = {} as any;\n for (const [key, value] of Object.entries(filter)) {\n if (value && typeof value === \"object\" && !isPlainObject(value) && \"idTaggedMaybe\" in value) {\n subFilter[key] = value.idTaggedMaybe || nilIdValue(meta);\n } else {\n subFilter[key] = value;\n }\n }\n return { kind: \"join\", subFilter };\n } else {\n throw new Error(`Unrecognized filter ${filter}`);\n }\n}\n\n/**\n * Parses the many/hodgepodge (ergonomic!) patterns of value filters into a `ParsedValueFilter[]`.\n *\n * Note we return an array because filter might be a `ValueGraphQLFilter` that is allowed to have\n * multiple conditions, i.e. `{ lt: 10, gt: 5 }`.\n */\nexport function parseValueFilter<V>(filter: ValueFilter<V, any>): ParsedValueFilter<V>[] {\n if (filter === null) {\n return [{ kind: \"is-null\" }];\n } else if (filter === undefined) {\n // This is legacy behavior where `em.find(Book, { author: undefined })` would match all books\n return [];\n } else if (Array.isArray(filter)) {\n return [{ kind: \"in\", value: filter }];\n } else if (isPlainObject(filter)) {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n // Should this be an error?\n return [];\n } else if (keys.length === 2 && \"op\" in filter && \"value\" in filter) {\n // Probe for `findGql` op & value\n const { op, value } = filter;\n if (shouldPruneValueFilter(op, value)) {\n return [];\n } else if (value === null) {\n return [{ kind: \"is-null\" }];\n } else {\n return [{ kind: op, value: value ?? null }];\n }\n } else if (keys.length === 2 && \"gte\" in filter && \"lte\" in filter) {\n const { gte, lte } = filter;\n return [{ kind: \"between\", value: [gte, lte] }];\n } else {\n return Object.entries(filter)\n .map(([key, value]) => {\n // Always do condition pruning on the value\n if (shouldPruneValueFilter(key, value)) {\n return undefined;\n }\n switch (key) {\n case \"eq\":\n if (value === null) {\n return { kind: \"is-null\" as const };\n } else {\n return { kind: \"eq\" as const, value: filter[key] };\n }\n case \"ne\":\n if (value === null) {\n return { kind: \"not-null\" as const };\n } else {\n return { kind: \"ne\" as const, value: filter[key] ?? null };\n }\n case \"in\":\n case \"nin\":\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\":\n return { kind: key, value: filter[key] };\n case \"pathExists\":\n return { kind: \"jsonPathExists\" as const, value: filter[key] };\n case \"pathIsTrue\":\n return { kind: \"jsonPathPredicate\" as const, value: filter[key] };\n case \"search\":\n return { kind: \"ilike\" as const, value: makeLike(filter[key]) };\n case \"between\":\n return { kind: key, value: filter[key] };\n default:\n throw new Error(`Unsupported value filter key ${key}`);\n }\n })\n .filter(isDefined);\n }\n } else {\n // This is a primitive like a string, number\n return [{ kind: \"eq\", value: filter }];\n }\n}\n\n/** Converts domain-level values like string ids/enums into their db equivalent. */\nexport function mapToDb(column: Column, filter: ParsedValueFilter<any>): ParsedValueFilter<any> {\n // ...to teach this `mapToDb` function to handle/rewrite `in: [1, null]` handling, we'd need to:\n // 1. return a maybe-simple/maybe-nested condition, so basically a `ParsedExpressionCondition`, because\n // this would let `in` return an `{ or: ... }` to all the callers.\n // 2. also return `{ parsed: ParsedExpressionCondition, simples: SimpleCondition[] }` tuple, for the\n // alias `addCondition` processing to track the `simples` and rewrite their alias when later bound.\n switch (filter.kind) {\n case \"eq\":\n case \"gt\":\n case \"gte\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n filter.value = column.mapToDb(filter.value);\n return filter;\n case \"in\":\n if (column.isArray) {\n // Arrays need a special operator\n return { kind: \"contains\", value: column.mapToDb(filter.value) };\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"nin\":\n if (column.isArray) {\n // Arrays need a special operator\n throw new Error(\"The nin operator is not supported on array columns yet\");\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"contains\":\n case \"ncontains\":\n case \"overlaps\":\n case \"noverlaps\":\n case \"containedBy\":\n const supportsContains = column.isArray || column.dbType === \"jsonb\";\n if (!supportsContains) {\n throw new Error(`${filter.kind} is only unsupported on array or jsonb columns`);\n }\n if (column.isArray) {\n filter.value = column.mapToDb(filter.value);\n } else {\n // leave jsonb values alone\n }\n return filter;\n case \"between\":\n filter.value = [column.mapToDb(filter.value[0]), column.mapToDb(filter.value[1])];\n return filter;\n case \"is-null\":\n case \"not-null\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n return filter;\n default:\n throw assertNever(filter);\n }\n}\n\n/** Converts a search term like `foo bar` into a SQL `like` pattern like `%foo%bar%`. */\nexport function makeLike(search: any | undefined): any {\n return search ? `%${search.replace(/\\s+/g, \"%\")}%` : undefined;\n}\n\n/** Recognizes placeholder IDs used when a filter contains an unpersisted entity. */\nexport function isNilIdValue(value: unknown): boolean {\n return value === -1 || value === \"00000000-0000-0000-0000-000000000000\";\n}\n\n/**\n * We use this value if users include new (id-less) entities as em.find conditions.\n *\n * The idea is that this condition would never be met, but we still want to do the em.find\n * query in case it's in an `OR` clause that would match false, but some other part of the\n * clause would match. I.e. instead of just skipping the DB query all together, which is\n * also something we could consider doing.\n *\n * For int IDs we use -1, and for uuid IDs, we use the nil UUID value:\n *\n * https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID\n */\nfunction nilIdValue(meta: EntityMetadata): any {\n switch (meta.idDbType) {\n case \"int\":\n case \"bigint\":\n return -1;\n case \"uuid\":\n return \"00000000-0000-0000-0000-000000000000\";\n case \"text\":\n return \"0\";\n default:\n return assertNever(meta.idDbType);\n }\n}\n\n/** Returns true for values that should be treated like an omitted filter. */\nfunction shouldPruneValueFilter(key: unknown, value: unknown): boolean {\n if (value === undefined) return true;\n return value === false && key !== \"eq\" && key !== \"ne\";\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valueFilters.d.cts","names":[],"sources":["../../src/queries/valueFilters.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"valueFilters.d.cts","names":[],"sources":["../../src/queries/valueFilters.ts"],"mappings":";;;;;;KAYY,qBAER;EAEE;EAAc;;;iBAGJ,kBAAkB,MAAM,gBAAgB,cAAc;;;;;;;iBAsFtD,iBAAiB,GAAG,QAAQ,YAAY,UAAU,kBAAkB;;iBAqFpE,QAAQ,QAAQ,QAAQ,QAAQ,yBAAyB;;iBAoEzD,SAAS;;iBAKT,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valueFilters.d.mts","names":[],"sources":["../../src/queries/valueFilters.ts"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"valueFilters.d.mts","names":[],"sources":["../../src/queries/valueFilters.ts"],"mappings":";;;;;;KAYY,qBAER;EAEE;EAAc;;;iBAGJ,kBAAkB,MAAM,gBAAgB,cAAc;;;;;;;iBAsFtD,iBAAiB,GAAG,QAAQ,YAAY,UAAU,kBAAkB;;iBAqFpE,QAAQ,QAAQ,QAAQ,QAAQ,yBAAyB;;iBAoEzD,SAAS;;iBAKT,aAAa"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valueFilters.js","names":[],"sources":["../../src/queries/valueFilters.ts"],"sourcesContent":["import { isPlainObject } from \"joist-utils\";\n\nimport { type Entity, isEntity } from \"../Entity.ts\";\nimport { isDefined } from \"../EntityManager.ts\";\nimport type { EntityMetadata } from \"../EntityMetadata.ts\";\nimport type { Column } from \"../serde/columns.ts\";\nimport { assertNever } from \"../utils.ts\";\nimport { isAlias } from \"./find/Aliases.ts\";\nimport type { ValueFilter } from \"./find/EntityFilter.ts\";\nimport { isScope } from \"./find/scopes.ts\";\nimport type { ParsedValueFilter } from \"./parsedConditions.ts\";\n\n/** An ADT version of `EntityFilter`. */\nexport type ParsedEntityFilter =\n // ParsedValueFilter is any simple match on `id`\n | ParsedValueFilter<string | number>\n // Otherwise we return the join/complex\n | { kind: \"join\"; subFilter: object };\n\n/** Parses an entity filter, which could be \"just an id\", an array of ids, or a nested filter. */\nexport function parseEntityFilter(meta: EntityMetadata, filter: any): ParsedEntityFilter | undefined {\n if (filter === undefined) {\n // This matches legacy `em.find(Book, { author: undefined })` behavior\n return undefined;\n } else if (isAlias(filter)) {\n // We're just binding an alias to this position in the join tree\n return undefined;\n } else if (isScope(filter)) {\n // A scope on a relation (e.g. `{ author: Author.adult }`) always implies conditions on the\n // related entity, so signal \"needs a join\" via the existing `join` discriminant rather than\n // adding a dedicated `kind: \"scope\"` that every relation branch would have to learn. The empty\n // `subFilter` is intentionally never read: the m2o/m2m branches only check `kind === \"join\"` to\n // decide whether to join, then re-dispatch the original scope value back through `addTable`,\n // where `isScope(...)` routes it to `addScopeFilterAt` to apply the scope's actual fragments.\n return { kind: \"join\", subFilter: {} };\n } else if (filter === null) {\n return { kind: \"is-null\" };\n } else if (typeof filter === \"string\" || typeof filter === \"number\") {\n return { kind: \"eq\", value: filter };\n } else if (typeof filter === \"boolean\") {\n return filter ? { kind: \"not-null\" } : { kind: \"is-null\" };\n } else if (Array.isArray(filter)) {\n return {\n kind: \"in\",\n value: filter.map((v: string | number | Entity) => {\n return isEntity(v) ? (v.idTaggedMaybe ?? nilIdValue(meta)) : v;\n }),\n };\n } else if (isEntity(filter)) {\n return { kind: \"eq\", value: filter.idTaggedMaybe || nilIdValue(meta) };\n } else if (typeof filter === \"object\") {\n // Looking for `{ firstName: \"f1\" }` or `{ ne: \"f1\" }`\n const keys = Object.keys(filter);\n // Special case only looking at `ne`\n if (keys.length === 1 && keys[0] === \"ne\") {\n const value = filter[\"ne\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"not-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"ne\", value };\n } else if (isEntity(value)) {\n return { kind: \"ne\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n throw new Error(`Unsupported \"ne\" value ${value}`);\n }\n }\n // Special case only looking at `id`\n if (keys.length === 1 && keys[0] === \"id\") {\n const value = filter[\"id\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"is-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"eq\", value };\n } else if (isEntity(value)) {\n return { kind: \"eq\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n return parseValueFilter(value)[0] as any;\n }\n }\n // Look for subFilter values being EntityFilter-ish instances like ManyToOneReference\n // that have an id, and so structurally match the entity filter without really being filters,\n // and convert them over here before getting into parseValueFilter.\n const subFilter = {} as any;\n for (const [key, value] of Object.entries(filter)) {\n if (value && typeof value === \"object\" && !isPlainObject(value) && \"idTaggedMaybe\" in value) {\n subFilter[key] = value.idTaggedMaybe || nilIdValue(meta);\n } else {\n subFilter[key] = value;\n }\n }\n return { kind: \"join\", subFilter };\n } else {\n throw new Error(`Unrecognized filter ${filter}`);\n }\n}\n\n/**\n * Parses the many/hodgepodge (ergonomic!) patterns of value filters into a `ParsedValueFilter[]`.\n *\n * Note we return an array because filter might be a `ValueGraphQLFilter` that is allowed to have\n * multiple conditions, i.e. `{ lt: 10, gt: 5 }`.\n */\nexport function parseValueFilter<V>(filter: ValueFilter<V, any>): ParsedValueFilter<V>[] {\n if (filter === null) {\n return [{ kind: \"is-null\" }];\n } else if (filter === undefined) {\n // This is legacy behavior where `em.find(Book, { author: undefined })` would match all books\n return [];\n } else if (Array.isArray(filter)) {\n return [{ kind: \"in\", value: filter }];\n } else if (isPlainObject(filter)) {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n // Should this be an error?\n return [];\n } else if (keys.length === 2 && \"op\" in filter && \"value\" in filter) {\n // Probe for `findGql` op & value\n const { op, value } = filter;\n if (shouldPruneValueFilter(op, value)) {\n return [];\n } else if (value === null) {\n return [{ kind: \"is-null\" }];\n } else {\n return [{ kind: op, value: value ?? null }];\n }\n } else if (keys.length === 2 && \"gte\" in filter && \"lte\" in filter) {\n const { gte, lte } = filter;\n return [{ kind: \"between\", value: [gte, lte] }];\n } else {\n return Object.entries(filter)\n .map(([key, value]) => {\n // Always do condition pruning on the value\n if (shouldPruneValueFilter(key, value)) {\n return undefined;\n }\n switch (key) {\n case \"eq\":\n if (value === null) {\n return { kind: \"is-null\" as const };\n } else {\n return { kind: \"eq\" as const, value: filter[key] };\n }\n case \"ne\":\n if (value === null) {\n return { kind: \"not-null\" as const };\n } else {\n return { kind: \"ne\" as const, value: filter[key] ?? null };\n }\n case \"in\":\n case \"nin\":\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\":\n return { kind: key, value: filter[key] };\n case \"pathExists\":\n return { kind: \"jsonPathExists\" as const, value: filter[key] };\n case \"pathIsTrue\":\n return { kind: \"jsonPathPredicate\" as const, value: filter[key] };\n case \"search\":\n return { kind: \"ilike\" as const, value: makeLike(filter[key]) };\n case \"between\":\n return { kind: key, value: filter[key] };\n default:\n throw new Error(`Unsupported value filter key ${key}`);\n }\n })\n .filter(isDefined);\n }\n } else {\n // This is a primitive like a string, number\n return [{ kind: \"eq\", value: filter }];\n }\n}\n\n/** Converts domain-level values like string ids/enums into their db equivalent. */\nexport function mapToDb(column: Column, filter: ParsedValueFilter<any>): ParsedValueFilter<any> {\n // ...to teach this `mapToDb` function to handle/rewrite `in: [1, null]` handling, we'd need to:\n // 1. return a maybe-simple/maybe-nested condition, so basically a `ParsedExpressionCondition`, because\n // this would let `in` return an `{ or: ... }` to all the callers.\n // 2. also return `{ parsed: ParsedExpressionCondition, simples: SimpleCondition[] }` tuple, for the\n // alias `addCondition` processing to track the `simples` and rewrite their alias when later bound.\n switch (filter.kind) {\n case \"eq\":\n case \"gt\":\n case \"gte\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n filter.value = column.mapToDb(filter.value);\n return filter;\n case \"in\":\n if (column.isArray) {\n // Arrays need a special operator\n return { kind: \"contains\", value: column.mapToDb(filter.value) };\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"nin\":\n if (column.isArray) {\n // Arrays need a special operator\n throw new Error(\"The nin operator is not supported on array columns yet\");\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"contains\":\n case \"ncontains\":\n case \"overlaps\":\n case \"noverlaps\":\n case \"containedBy\":\n const supportsContains = column.isArray || column.dbType === \"jsonb\";\n if (!supportsContains) {\n throw new Error(`${filter.kind} is only unsupported on array or jsonb columns`);\n }\n if (column.isArray) {\n filter.value = column.mapToDb(filter.value);\n } else {\n // leave jsonb values alone\n }\n return filter;\n case \"between\":\n filter.value = [column.mapToDb(filter.value[0]), column.mapToDb(filter.value[1])];\n return filter;\n case \"is-null\":\n case \"not-null\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n return filter;\n default:\n throw assertNever(filter);\n }\n}\n\n/** Converts a search term like `foo bar` into a SQL `like` pattern like `%foo%bar%`. */\nexport function makeLike(search: any | undefined): any {\n return search ? `%${search.replace(/\\s+/g, \"%\")}%` : undefined;\n}\n\n/** Recognizes placeholder IDs used when a filter contains an unpersisted entity. */\nexport function isNilIdValue(value: unknown): boolean {\n return value === -1 || value === \"00000000-0000-0000-0000-000000000000\";\n}\n\n/**\n * We use this value if users include new (id-less) entities as em.find conditions.\n *\n * The idea is that this condition would never be met, but we still want to do the em.find\n * query in case it's in an `OR` clause that would match false, but some other part of the\n * clause would match. I.e. instead of just skipping the DB query all together, which is\n * also something we could consider doing.\n *\n * For int IDs we use -1, and for uuid IDs, we use the nil UUID value:\n *\n * https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID\n */\nfunction nilIdValue(meta: EntityMetadata): any {\n switch (meta.idDbType) {\n case \"int\":\n case \"bigint\":\n return -1;\n case \"uuid\":\n return \"00000000-0000-0000-0000-000000000000\";\n case \"text\":\n return \"0\";\n default:\n return assertNever(meta.idDbType);\n }\n}\n\n/** Returns true for values that should be treated like an omitted filter. */\nfunction shouldPruneValueFilter(key: unknown, value: unknown): boolean {\n if (value === undefined) return true;\n return value === false && key !== \"eq\" && key !== \"ne\";\n}\n"],"mappings":";;;;;;;;AAoBA,SAAgB,kBAAkB,MAAsB,QAA6C;CACnG,IAAI,WAAW,KAAA,GAEb;MACK,IAAI,QAAQ,MAAM,GAEvB;MACK,IAAI,QAAQ,MAAM,GAOvB,OAAO;EAAE,MAAM;EAAQ,WAAW,CAAC;CAAE;MAChC,IAAI,WAAW,MACpB,OAAO,EAAE,MAAM,UAAU;MACpB,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,UACzD,OAAO;EAAE,MAAM;EAAM,OAAO;CAAO;MAC9B,IAAI,OAAO,WAAW,WAC3B,OAAO,SAAS,EAAE,MAAM,WAAW,IAAI,EAAE,MAAM,UAAU;MACpD,IAAI,MAAM,QAAQ,MAAM,GAC7B,OAAO;EACL,MAAM;EACN,OAAO,OAAO,KAAK,MAAgC;GACjD,OAAO,SAAS,CAAC,IAAK,EAAE,iBAAiB,WAAW,IAAI,IAAK;EAC/D,CAAC;CACH;MACK,IAAI,SAAS,MAAM,GACxB,OAAO;EAAE,MAAM;EAAM,OAAO,OAAO,iBAAiB,WAAW,IAAI;CAAE;MAChE,IAAI,OAAO,WAAW,UAAU;EAErC,MAAM,OAAO,OAAO,KAAK,MAAM;EAE/B,IAAI,KAAK,WAAW,KAAK,KAAK,OAAO,MAAM;GACzC,MAAM,QAAQ,OAAO;GACrB,IAAI,UAAU,KAAA,GACZ;QACK,IAAI,UAAU,MACnB,OAAO,EAAE,MAAM,WAAW;QACrB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UACvD,OAAO;IAAE,MAAM;IAAM;GAAM;QACtB,IAAI,SAAS,KAAK,GACvB,OAAO;IAAE,MAAM;IAAM,OAAO,MAAM,iBAAiB,WAAW,IAAI;GAAE;QAEpE,MAAM,IAAI,MAAM,0BAA0B,OAAO;EAErD;EAEA,IAAI,KAAK,WAAW,KAAK,KAAK,OAAO,MAAM;GACzC,MAAM,QAAQ,OAAO;GACrB,IAAI,UAAU,KAAA,GACZ;QACK,IAAI,UAAU,MACnB,OAAO,EAAE,MAAM,UAAU;QACpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UACvD,OAAO;IAAE,MAAM;IAAM;GAAM;QACtB,IAAI,SAAS,KAAK,GACvB,OAAO;IAAE,MAAM;IAAM,OAAO,MAAM,iBAAiB,WAAW,IAAI;GAAE;QAEpE,OAAO,iBAAiB,KAAK,CAAC,CAAC;EAEnC;EAIA,MAAM,YAAY,CAAC;EACnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,SAAS,OAAO,UAAU,YAAY,CAAC,cAAc,KAAK,KAAK,mBAAmB,OACpF,UAAU,OAAO,MAAM,iBAAiB,WAAW,IAAI;OAEvD,UAAU,OAAO;EAGrB,OAAO;GAAE,MAAM;GAAQ;EAAU;CACnC,OACE,MAAM,IAAI,MAAM,uBAAuB,QAAQ;AAEnD;;;;;;;AAQA,SAAgB,iBAAoB,QAAqD;CACvF,IAAI,WAAW,MACb,OAAO,CAAC,EAAE,MAAM,UAAU,CAAC;MACtB,IAAI,WAAW,KAAA,GAEpB,OAAO,CAAC;MACH,IAAI,MAAM,QAAQ,MAAM,GAC7B,OAAO,CAAC;EAAE,MAAM;EAAM,OAAO;CAAO,CAAC;MAChC,IAAI,cAAc,MAAM,GAAG;EAChC,MAAM,OAAO,OAAO,KAAK,MAAM;EAC/B,IAAI,KAAK,WAAW,GAElB,OAAO,CAAC;OACH,IAAI,KAAK,WAAW,KAAK,QAAQ,UAAU,WAAW,QAAQ;GAEnE,MAAM,EAAE,IAAI,UAAU;GACtB,IAAI,uBAAuB,IAAI,KAAK,GAClC,OAAO,CAAC;QACH,IAAI,UAAU,MACnB,OAAO,CAAC,EAAE,MAAM,UAAU,CAAC;QAE3B,OAAO,CAAC;IAAE,MAAM;IAAI,OAAO,SAAS;GAAK,CAAC;EAE9C,OAAO,IAAI,KAAK,WAAW,KAAK,SAAS,UAAU,SAAS,QAAQ;GAClE,MAAM,EAAE,KAAK,QAAQ;GACrB,OAAO,CAAC;IAAE,MAAM;IAAW,OAAO,CAAC,KAAK,GAAG;GAAE,CAAC;EAChD,OACE,OAAO,OAAO,QAAQ,MAAM,CAAC,CAC1B,KAAK,CAAC,KAAK,WAAW;GAErB,IAAI,uBAAuB,KAAK,KAAK,GACnC;GAEF,QAAQ,KAAR;IACE,KAAK,MACH,IAAI,UAAU,MACZ,OAAO,EAAE,MAAM,UAAmB;SAElC,OAAO;KAAE,MAAM;KAAe,OAAO,OAAO;IAAK;IAErD,KAAK,MACH,IAAI,UAAU,MACZ,OAAO,EAAE,MAAM,WAAoB;SAEnC,OAAO;KAAE,MAAM;KAAe,OAAO,OAAO,QAAQ;IAAK;IAE7D,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK,eACH,OAAO;KAAE,MAAM;KAAK,OAAO,OAAO;IAAK;IACzC,KAAK,cACH,OAAO;KAAE,MAAM;KAA2B,OAAO,OAAO;IAAK;IAC/D,KAAK,cACH,OAAO;KAAE,MAAM;KAA8B,OAAO,OAAO;IAAK;IAClE,KAAK,UACH,OAAO;KAAE,MAAM;KAAkB,OAAO,SAAS,OAAO,IAAI;IAAE;IAChE,KAAK,WACH,OAAO;KAAE,MAAM;KAAK,OAAO,OAAO;IAAK;IACzC,SACE,MAAM,IAAI,MAAM,gCAAgC,KAAK;GACzD;EACF,CAAC,CAAC,CACD,OAAO,SAAS;CAEvB,OAEE,OAAO,CAAC;EAAE,MAAM;EAAM,OAAO;CAAO,CAAC;AAEzC;;AAGA,SAAgB,QAAQ,QAAgB,QAAwD;CAM9F,QAAQ,OAAO,MAAf;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GACH,OAAO,QAAQ,OAAO,QAAQ,OAAO,KAAK;GAC1C,OAAO;EACT,KAAK;GACH,IAAI,OAAO,SAET,OAAO;IAAE,MAAM;IAAY,OAAO,OAAO,QAAQ,OAAO,KAAK;GAAE;QAE/D,OAAO,QAAQ,OAAO,MAAM,KAAK,MAAM,OAAO,QAAQ,CAAC,CAAC;GAE1D,OAAO;EACT,KAAK;GACH,IAAI,OAAO,SAET,MAAM,IAAI,MAAM,wDAAwD;QAExE,OAAO,QAAQ,OAAO,MAAM,KAAK,MAAM,OAAO,QAAQ,CAAC,CAAC;GAE1D,OAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GAEH,IAAI,EADqB,OAAO,WAAW,OAAO,WAAW,UAE3D,MAAM,IAAI,MAAM,GAAG,OAAO,KAAK,+CAA+C;GAEhF,IAAI,OAAO,SACT,OAAO,QAAQ,OAAO,QAAQ,OAAO,KAAK;GAI5C,OAAO;EACT,KAAK;GACH,OAAO,QAAQ,CAAC,OAAO,QAAQ,OAAO,MAAM,EAAE,GAAG,OAAO,QAAQ,OAAO,MAAM,EAAE,CAAC;GAChF,OAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,qBACH,OAAO;EACT,SACE,MAAM,YAAY,MAAM;CAC5B;AACF;;AAGA,SAAgB,SAAS,QAA8B;CACrD,OAAO,SAAS,IAAI,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,KAAA;AACvD;;AAGA,SAAgB,aAAa,OAAyB;CACpD,OAAO,UAAU,MAAM,UAAU;AACnC;;;;;;;;;;;;;AAcA,SAAS,WAAW,MAA2B;CAC7C,QAAQ,KAAK,UAAb;EACE,KAAK;EACL,KAAK,UACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE,OAAO,YAAY,KAAK,QAAQ;CACpC;AACF;;AAGA,SAAS,uBAAuB,KAAc,OAAyB;CACrE,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,OAAO,UAAU,SAAS,QAAQ,QAAQ,QAAQ;AACpD"}
|
|
1
|
+
{"version":3,"file":"valueFilters.js","names":[],"sources":["../../src/queries/valueFilters.ts"],"sourcesContent":["import { isPlainObject } from \"joist-utils\";\nimport { type Entity, isEntity } from \"src/Entity.ts\";\nimport { isDefined } from \"src/EntityManager.ts\";\nimport type { EntityMetadata } from \"src/EntityMetadata.ts\";\nimport { isAlias } from \"src/queries/find/Aliases.ts\";\nimport type { ValueFilter } from \"src/queries/find/EntityFilter.ts\";\nimport { isScope } from \"src/queries/find/scopes.ts\";\nimport type { ParsedValueFilter } from \"src/queries/parsedConditions.ts\";\nimport type { Column } from \"src/serde/columns.ts\";\nimport { assertNever } from \"src/utils.ts\";\n\n/** An ADT version of `EntityFilter`. */\nexport type ParsedEntityFilter =\n // ParsedValueFilter is any simple match on `id`\n | ParsedValueFilter<string | number>\n // Otherwise we return the join/complex\n | { kind: \"join\"; subFilter: object };\n\n/** Parses an entity filter, which could be \"just an id\", an array of ids, or a nested filter. */\nexport function parseEntityFilter(meta: EntityMetadata, filter: any): ParsedEntityFilter | undefined {\n if (filter === undefined) {\n // This matches legacy `em.find(Book, { author: undefined })` behavior\n return undefined;\n } else if (isAlias(filter)) {\n // We're just binding an alias to this position in the join tree\n return undefined;\n } else if (isScope(filter)) {\n // A scope on a relation (e.g. `{ author: Author.adult }`) always implies conditions on the\n // related entity, so signal \"needs a join\" via the existing `join` discriminant rather than\n // adding a dedicated `kind: \"scope\"` that every relation branch would have to learn. The empty\n // `subFilter` is intentionally never read: the m2o/m2m branches only check `kind === \"join\"` to\n // decide whether to join, then re-dispatch the original scope value back through `addTable`,\n // where `isScope(...)` routes it to `addScopeFilterAt` to apply the scope's actual fragments.\n return { kind: \"join\", subFilter: {} };\n } else if (filter === null) {\n return { kind: \"is-null\" };\n } else if (typeof filter === \"string\" || typeof filter === \"number\") {\n return { kind: \"eq\", value: filter };\n } else if (typeof filter === \"boolean\") {\n return filter ? { kind: \"not-null\" } : { kind: \"is-null\" };\n } else if (Array.isArray(filter)) {\n return {\n kind: \"in\",\n value: filter.map((v: string | number | Entity) => {\n return isEntity(v) ? (v.idTaggedMaybe ?? nilIdValue(meta)) : v;\n }),\n };\n } else if (isEntity(filter)) {\n return { kind: \"eq\", value: filter.idTaggedMaybe || nilIdValue(meta) };\n } else if (typeof filter === \"object\") {\n // Looking for `{ firstName: \"f1\" }` or `{ ne: \"f1\" }`\n const keys = Object.keys(filter);\n // Special case only looking at `ne`\n if (keys.length === 1 && keys[0] === \"ne\") {\n const value = filter[\"ne\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"not-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"ne\", value };\n } else if (isEntity(value)) {\n return { kind: \"ne\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n throw new Error(`Unsupported \"ne\" value ${value}`);\n }\n }\n // Special case only looking at `id`\n if (keys.length === 1 && keys[0] === \"id\") {\n const value = filter[\"id\"];\n if (value === undefined) {\n return undefined;\n } else if (value === null) {\n return { kind: \"is-null\" };\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n return { kind: \"eq\", value };\n } else if (isEntity(value)) {\n return { kind: \"eq\", value: value.idTaggedMaybe || nilIdValue(meta) };\n } else {\n return parseValueFilter(value)[0] as any;\n }\n }\n // Look for subFilter values being EntityFilter-ish instances like ManyToOneReference\n // that have an id, and so structurally match the entity filter without really being filters,\n // and convert them over here before getting into parseValueFilter.\n const subFilter = {} as any;\n for (const [key, value] of Object.entries(filter)) {\n if (value && typeof value === \"object\" && !isPlainObject(value) && \"idTaggedMaybe\" in value) {\n subFilter[key] = value.idTaggedMaybe || nilIdValue(meta);\n } else {\n subFilter[key] = value;\n }\n }\n return { kind: \"join\", subFilter };\n } else {\n throw new Error(`Unrecognized filter ${filter}`);\n }\n}\n\n/**\n * Parses the many/hodgepodge (ergonomic!) patterns of value filters into a `ParsedValueFilter[]`.\n *\n * Note we return an array because filter might be a `ValueGraphQLFilter` that is allowed to have\n * multiple conditions, i.e. `{ lt: 10, gt: 5 }`.\n */\nexport function parseValueFilter<V>(filter: ValueFilter<V, any>): ParsedValueFilter<V>[] {\n if (filter === null) {\n return [{ kind: \"is-null\" }];\n } else if (filter === undefined) {\n // This is legacy behavior where `em.find(Book, { author: undefined })` would match all books\n return [];\n } else if (Array.isArray(filter)) {\n return [{ kind: \"in\", value: filter }];\n } else if (isPlainObject(filter)) {\n const keys = Object.keys(filter);\n if (keys.length === 0) {\n // Should this be an error?\n return [];\n } else if (keys.length === 2 && \"op\" in filter && \"value\" in filter) {\n // Probe for `findGql` op & value\n const { op, value } = filter;\n if (shouldPruneValueFilter(op, value)) {\n return [];\n } else if (value === null) {\n return [{ kind: \"is-null\" }];\n } else {\n return [{ kind: op, value: value ?? null }];\n }\n } else if (keys.length === 2 && \"gte\" in filter && \"lte\" in filter) {\n const { gte, lte } = filter;\n return [{ kind: \"between\", value: [gte, lte] }];\n } else {\n return Object.entries(filter)\n .map(([key, value]) => {\n // Always do condition pruning on the value\n if (shouldPruneValueFilter(key, value)) {\n return undefined;\n }\n switch (key) {\n case \"eq\":\n if (value === null) {\n return { kind: \"is-null\" as const };\n } else {\n return { kind: \"eq\" as const, value: filter[key] };\n }\n case \"ne\":\n if (value === null) {\n return { kind: \"not-null\" as const };\n } else {\n return { kind: \"ne\" as const, value: filter[key] ?? null };\n }\n case \"in\":\n case \"nin\":\n case \"gt\":\n case \"gte\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n case \"contains\":\n case \"overlaps\":\n case \"containedBy\":\n return { kind: key, value: filter[key] };\n case \"pathExists\":\n return { kind: \"jsonPathExists\" as const, value: filter[key] };\n case \"pathIsTrue\":\n return { kind: \"jsonPathPredicate\" as const, value: filter[key] };\n case \"search\":\n return { kind: \"ilike\" as const, value: makeLike(filter[key]) };\n case \"between\":\n return { kind: key, value: filter[key] };\n default:\n throw new Error(`Unsupported value filter key ${key}`);\n }\n })\n .filter(isDefined);\n }\n } else {\n // This is a primitive like a string, number\n return [{ kind: \"eq\", value: filter }];\n }\n}\n\n/** Converts domain-level values like string ids/enums into their db equivalent. */\nexport function mapToDb(column: Column, filter: ParsedValueFilter<any>): ParsedValueFilter<any> {\n // ...to teach this `mapToDb` function to handle/rewrite `in: [1, null]` handling, we'd need to:\n // 1. return a maybe-simple/maybe-nested condition, so basically a `ParsedExpressionCondition`, because\n // this would let `in` return an `{ or: ... }` to all the callers.\n // 2. also return `{ parsed: ParsedExpressionCondition, simples: SimpleCondition[] }` tuple, for the\n // alias `addCondition` processing to track the `simples` and rewrite their alias when later bound.\n switch (filter.kind) {\n case \"eq\":\n case \"gt\":\n case \"gte\":\n case \"ne\":\n case \"lt\":\n case \"lte\":\n case \"like\":\n case \"nlike\":\n case \"ilike\":\n case \"nilike\":\n case \"regex\":\n case \"nregex\":\n case \"iregex\":\n case \"niregex\":\n filter.value = column.mapToDb(filter.value);\n return filter;\n case \"in\":\n if (column.isArray) {\n // Arrays need a special operator\n return { kind: \"contains\", value: column.mapToDb(filter.value) };\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"nin\":\n if (column.isArray) {\n // Arrays need a special operator\n throw new Error(\"The nin operator is not supported on array columns yet\");\n } else {\n filter.value = filter.value.map((v) => column.mapToDb(v));\n }\n return filter;\n case \"contains\":\n case \"ncontains\":\n case \"overlaps\":\n case \"noverlaps\":\n case \"containedBy\":\n const supportsContains = column.isArray || column.dbType === \"jsonb\";\n if (!supportsContains) {\n throw new Error(`${filter.kind} is only unsupported on array or jsonb columns`);\n }\n if (column.isArray) {\n filter.value = column.mapToDb(filter.value);\n } else {\n // leave jsonb values alone\n }\n return filter;\n case \"between\":\n filter.value = [column.mapToDb(filter.value[0]), column.mapToDb(filter.value[1])];\n return filter;\n case \"is-null\":\n case \"not-null\":\n case \"jsonPathExists\":\n case \"jsonPathPredicate\":\n return filter;\n default:\n throw assertNever(filter);\n }\n}\n\n/** Converts a search term like `foo bar` into a SQL `like` pattern like `%foo%bar%`. */\nexport function makeLike(search: any | undefined): any {\n return search ? `%${search.replace(/\\s+/g, \"%\")}%` : undefined;\n}\n\n/** Recognizes placeholder IDs used when a filter contains an unpersisted entity. */\nexport function isNilIdValue(value: unknown): boolean {\n return value === -1 || value === \"00000000-0000-0000-0000-000000000000\";\n}\n\n/**\n * We use this value if users include new (id-less) entities as em.find conditions.\n *\n * The idea is that this condition would never be met, but we still want to do the em.find\n * query in case it's in an `OR` clause that would match false, but some other part of the\n * clause would match. I.e. instead of just skipping the DB query all together, which is\n * also something we could consider doing.\n *\n * For int IDs we use -1, and for uuid IDs, we use the nil UUID value:\n *\n * https://en.wikipedia.org/wiki/Universally_unique_identifier#Nil_UUID\n */\nfunction nilIdValue(meta: EntityMetadata): any {\n switch (meta.idDbType) {\n case \"int\":\n case \"bigint\":\n return -1;\n case \"uuid\":\n return \"00000000-0000-0000-0000-000000000000\";\n case \"text\":\n return \"0\";\n default:\n return assertNever(meta.idDbType);\n }\n}\n\n/** Returns true for values that should be treated like an omitted filter. */\nfunction shouldPruneValueFilter(key: unknown, value: unknown): boolean {\n if (value === undefined) return true;\n return value === false && key !== \"eq\" && key !== \"ne\";\n}\n"],"mappings":";;;;;;;;AAmBA,SAAgB,kBAAkB,MAAsB,QAA6C;CACnG,IAAI,WAAW,KAAA,GAEb;MACK,IAAI,QAAQ,MAAM,GAEvB;MACK,IAAI,QAAQ,MAAM,GAOvB,OAAO;EAAE,MAAM;EAAQ,WAAW,CAAC;CAAE;MAChC,IAAI,WAAW,MACpB,OAAO,EAAE,MAAM,UAAU;MACpB,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,UACzD,OAAO;EAAE,MAAM;EAAM,OAAO;CAAO;MAC9B,IAAI,OAAO,WAAW,WAC3B,OAAO,SAAS,EAAE,MAAM,WAAW,IAAI,EAAE,MAAM,UAAU;MACpD,IAAI,MAAM,QAAQ,MAAM,GAC7B,OAAO;EACL,MAAM;EACN,OAAO,OAAO,KAAK,MAAgC;GACjD,OAAO,SAAS,CAAC,IAAK,EAAE,iBAAiB,WAAW,IAAI,IAAK;EAC/D,CAAC;CACH;MACK,IAAI,SAAS,MAAM,GACxB,OAAO;EAAE,MAAM;EAAM,OAAO,OAAO,iBAAiB,WAAW,IAAI;CAAE;MAChE,IAAI,OAAO,WAAW,UAAU;EAErC,MAAM,OAAO,OAAO,KAAK,MAAM;EAE/B,IAAI,KAAK,WAAW,KAAK,KAAK,OAAO,MAAM;GACzC,MAAM,QAAQ,OAAO;GACrB,IAAI,UAAU,KAAA,GACZ;QACK,IAAI,UAAU,MACnB,OAAO,EAAE,MAAM,WAAW;QACrB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UACvD,OAAO;IAAE,MAAM;IAAM;GAAM;QACtB,IAAI,SAAS,KAAK,GACvB,OAAO;IAAE,MAAM;IAAM,OAAO,MAAM,iBAAiB,WAAW,IAAI;GAAE;QAEpE,MAAM,IAAI,MAAM,0BAA0B,OAAO;EAErD;EAEA,IAAI,KAAK,WAAW,KAAK,KAAK,OAAO,MAAM;GACzC,MAAM,QAAQ,OAAO;GACrB,IAAI,UAAU,KAAA,GACZ;QACK,IAAI,UAAU,MACnB,OAAO,EAAE,MAAM,UAAU;QACpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UACvD,OAAO;IAAE,MAAM;IAAM;GAAM;QACtB,IAAI,SAAS,KAAK,GACvB,OAAO;IAAE,MAAM;IAAM,OAAO,MAAM,iBAAiB,WAAW,IAAI;GAAE;QAEpE,OAAO,iBAAiB,KAAK,CAAC,CAAC;EAEnC;EAIA,MAAM,YAAY,CAAC;EACnB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,SAAS,OAAO,UAAU,YAAY,CAAC,cAAc,KAAK,KAAK,mBAAmB,OACpF,UAAU,OAAO,MAAM,iBAAiB,WAAW,IAAI;OAEvD,UAAU,OAAO;EAGrB,OAAO;GAAE,MAAM;GAAQ;EAAU;CACnC,OACE,MAAM,IAAI,MAAM,uBAAuB,QAAQ;AAEnD;;;;;;;AAQA,SAAgB,iBAAoB,QAAqD;CACvF,IAAI,WAAW,MACb,OAAO,CAAC,EAAE,MAAM,UAAU,CAAC;MACtB,IAAI,WAAW,KAAA,GAEpB,OAAO,CAAC;MACH,IAAI,MAAM,QAAQ,MAAM,GAC7B,OAAO,CAAC;EAAE,MAAM;EAAM,OAAO;CAAO,CAAC;MAChC,IAAI,cAAc,MAAM,GAAG;EAChC,MAAM,OAAO,OAAO,KAAK,MAAM;EAC/B,IAAI,KAAK,WAAW,GAElB,OAAO,CAAC;OACH,IAAI,KAAK,WAAW,KAAK,QAAQ,UAAU,WAAW,QAAQ;GAEnE,MAAM,EAAE,IAAI,UAAU;GACtB,IAAI,uBAAuB,IAAI,KAAK,GAClC,OAAO,CAAC;QACH,IAAI,UAAU,MACnB,OAAO,CAAC,EAAE,MAAM,UAAU,CAAC;QAE3B,OAAO,CAAC;IAAE,MAAM;IAAI,OAAO,SAAS;GAAK,CAAC;EAE9C,OAAO,IAAI,KAAK,WAAW,KAAK,SAAS,UAAU,SAAS,QAAQ;GAClE,MAAM,EAAE,KAAK,QAAQ;GACrB,OAAO,CAAC;IAAE,MAAM;IAAW,OAAO,CAAC,KAAK,GAAG;GAAE,CAAC;EAChD,OACE,OAAO,OAAO,QAAQ,MAAM,CAAC,CAC1B,KAAK,CAAC,KAAK,WAAW;GAErB,IAAI,uBAAuB,KAAK,KAAK,GACnC;GAEF,QAAQ,KAAR;IACE,KAAK,MACH,IAAI,UAAU,MACZ,OAAO,EAAE,MAAM,UAAmB;SAElC,OAAO;KAAE,MAAM;KAAe,OAAO,OAAO;IAAK;IAErD,KAAK,MACH,IAAI,UAAU,MACZ,OAAO,EAAE,MAAM,WAAoB;SAEnC,OAAO;KAAE,MAAM;KAAe,OAAO,OAAO,QAAQ;IAAK;IAE7D,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK,eACH,OAAO;KAAE,MAAM;KAAK,OAAO,OAAO;IAAK;IACzC,KAAK,cACH,OAAO;KAAE,MAAM;KAA2B,OAAO,OAAO;IAAK;IAC/D,KAAK,cACH,OAAO;KAAE,MAAM;KAA8B,OAAO,OAAO;IAAK;IAClE,KAAK,UACH,OAAO;KAAE,MAAM;KAAkB,OAAO,SAAS,OAAO,IAAI;IAAE;IAChE,KAAK,WACH,OAAO;KAAE,MAAM;KAAK,OAAO,OAAO;IAAK;IACzC,SACE,MAAM,IAAI,MAAM,gCAAgC,KAAK;GACzD;EACF,CAAC,CAAC,CACD,OAAO,SAAS;CAEvB,OAEE,OAAO,CAAC;EAAE,MAAM;EAAM,OAAO;CAAO,CAAC;AAEzC;;AAGA,SAAgB,QAAQ,QAAgB,QAAwD;CAM9F,QAAQ,OAAO,MAAf;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GACH,OAAO,QAAQ,OAAO,QAAQ,OAAO,KAAK;GAC1C,OAAO;EACT,KAAK;GACH,IAAI,OAAO,SAET,OAAO;IAAE,MAAM;IAAY,OAAO,OAAO,QAAQ,OAAO,KAAK;GAAE;QAE/D,OAAO,QAAQ,OAAO,MAAM,KAAK,MAAM,OAAO,QAAQ,CAAC,CAAC;GAE1D,OAAO;EACT,KAAK;GACH,IAAI,OAAO,SAET,MAAM,IAAI,MAAM,wDAAwD;QAExE,OAAO,QAAQ,OAAO,MAAM,KAAK,MAAM,OAAO,QAAQ,CAAC,CAAC;GAE1D,OAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GAEH,IAAI,EADqB,OAAO,WAAW,OAAO,WAAW,UAE3D,MAAM,IAAI,MAAM,GAAG,OAAO,KAAK,+CAA+C;GAEhF,IAAI,OAAO,SACT,OAAO,QAAQ,OAAO,QAAQ,OAAO,KAAK;GAI5C,OAAO;EACT,KAAK;GACH,OAAO,QAAQ,CAAC,OAAO,QAAQ,OAAO,MAAM,EAAE,GAAG,OAAO,QAAQ,OAAO,MAAM,EAAE,CAAC;GAChF,OAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,qBACH,OAAO;EACT,SACE,MAAM,YAAY,MAAM;CAC5B;AACF;;AAGA,SAAgB,SAAS,QAA8B;CACrD,OAAO,SAAS,IAAI,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,KAAA;AACvD;;AAGA,SAAgB,aAAa,OAAyB;CACpD,OAAO,UAAU,MAAM,UAAU;AACnC;;;;;;;;;;;;;AAcA,SAAS,WAAW,MAA2B;CAC7C,QAAQ,KAAK,UAAb;EACE,KAAK;EACL,KAAK,UACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,KAAK,QACH,OAAO;EACT,SACE,OAAO,YAAY,KAAK,QAAQ;CACpC;AACF;;AAGA,SAAS,uBAAuB,KAAc,OAAyB;CACrE,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,OAAO,UAAU,SAAS,QAAQ,QAAQ,QAAQ;AACpD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionsManager.cjs","names":["globalLogger","noopReactionLogger","#needsRecalc","getMetadata","followReverseHint","runInTrustedContext","#doAction","NoIdError","getEmInternalApi"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"../config.ts\";\nimport type { Entity } from \"../Entity.ts\";\nimport { type EntityManager, getEmInternalApi } from \"../EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"../EntityMetadata.ts\";\nimport { NoIdError } from \"../index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"../logging/ReactionLogger.ts\";\nimport { runInTrustedContext } from \"../trusted.ts\";\nimport { followReverseHint } from \"./reactiveHints.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears all the pending source fields, i.e. after `em.flush` is complete. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ReactionsManager.cjs","names":["globalLogger","noopReactionLogger","#needsRecalc","getMetadata","followReverseHint","runInTrustedContext","#doAction","NoIdError","getEmInternalApi"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"src/config.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { NoIdError } from \"src/index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"src/logging/ReactionLogger.ts\";\nimport { followReverseHint } from \"src/reactivity/reactiveHints.ts\";\nimport { runInTrustedContext } from \"src/trusted.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears all the pending source fields, i.e. after `em.flush` is complete. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ReactionsManager.js","names":["#needsRecalc","#doAction"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"../config.ts\";\nimport type { Entity } from \"../Entity.ts\";\nimport { type EntityManager, getEmInternalApi } from \"../EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"../EntityMetadata.ts\";\nimport { NoIdError } from \"../index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"../logging/ReactionLogger.ts\";\nimport { runInTrustedContext } from \"../trusted.ts\";\nimport { followReverseHint } from \"./reactiveHints.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears all the pending source fields, i.e. after `em.flush` is complete. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":";;;;;;;;;;;;;;AAiBA,IAAa,mBAAb,MAA8B;;CAE5B,oCAAsF,IAAI,IAAI;;CAE9F,2CAA6E,IAAI,IAAI;;;;;;;;;;CAUrF,8BAAgD,IAAI,IAAI;;;;;;;;;CASxD,mCAAqD,IAAI,IAAI;CAC7D,eAAe;EAAE,UAAU;EAAO,OAAO;EAAO,UAAU;CAAM;CAEhE,SAAyB,gBAAgB;CACzC;;CAEA,uBAAwC,CAAC;CAEzC,YAAY,IAAmB;EAC7B,KAAK,KAAK;CACZ;;;;;;;;CASA,0BAA0B,QAAgB,WAAyB;EAEjE,KAAK,MAAM,KAAK,KAAK,qBAAqB,QAAQ,SAAS,GAAG;GAS5D,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM;GAClC,KAAKA,aAAa,EAAE,QAAQ;GAC5B,KAAK,OAAO,UAAU,QAAQ,WAAW,CAAC;EAC5C;CACF;;CAGA,4BAA4B,QAAgB,WAAyB;EAEnE,KAAK,MAAM,KAAK,KAAK,qBAAqB,QAAQ,SAAS,GAAG;GAC5D,MAAM,UAAU,KAAK,WAAW,CAAC;GACjC,IAAI,QAAQ,KAAK,IAAI,MAAM,GAAG;IAQ5B,QAAQ,KAAK,IAAI,MAAM;IACvB,KAAKA,aAAa,EAAE,QAAQ;GAC9B,OAAO,IAAI,EAAE,OAAO,WAAW,GAK7B,QAAQ,KAAK,OAAO,MAAM;EAE9B;CACF;;CAGA,yBAAyB,QAAgB,QAAqC;EAC5E,KAAK,MAAM,KAAK,KAAK,cAAc,MAAM,GAAG;GAC1C,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM;GAClC,KAAKA,aAAa,EAAE,QAAQ;GAC5B,KAAK,OAAO,aAAa,QAAQ,QAAQ,CAAC;EAC5C;CACF;;;;;;;;;;;;;CAcA,qBAAqB,QAAgB,WAA4B;EAC/D,OAAO,KAAK,eAAe,YAAY,MAAM,CAAC,CAAC,CAAC,IAAI,SAAS;CAC/D;CAEA,4BAAqC;EACnC,OAAO,KAAKA,aAAa;CAC3B;;;;;;;;CASA,MAAM,wBAAwB,MAAwC;EACpE,IAAI,KAAK,YAAY,IAAI,GAAG,KAAK,OAAO,kBAAkB,KAAK,IAAI,IAAI;EAEvE,IAAI,QAAQ;EACZ,OAAO,KAAK,YAAY,IAAI,GAAG;GAK7B,IAAI,SAAS,cAAc;IACzB,KAAKA,aAAa,WAAW;IAC7B,KAAKA,aAAa,WAAW;GAC/B,OACE,KAAKA,aAAa,QAAQ;GAK5B,MAAM,uBAAiC,IAAI,IAAI;GAC/C,MAAM,UAA4B,CAAC;GAEnC,MAAM,QAAQ,MAAM,QAAQ,IAC1B,CAAC,GAAG,KAAK,kBAAkB,QAAQ,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,aAAa;IAEhE,IAAI,SAAS,gBAAgB,EAAE,SAAS,SAAS,OAAO,KAAA;IACxD,IAAI,SAAS,sBAAsB,EAAE,SAAS,cAAc,EAAE,SAAS,aAAa,OAAO,KAAA;IAE3F,MAAM,OAAO,CAAC,GAAG,QAAQ,IAAI;IAC7B,IAAI,KAAK,WAAW,GAAG,OAAO,KAAA;IAC9B,QAAQ,KAAK,MAAM;IACnB,KAAK,MAAM,SAAS,MAAM,QAAQ,KAAK,IAAI,KAAK;IAGhD,MAAM,sBADW,EAAE,KAAK,WAAW,IAAI,OAAO,MAAM,kBAAkB,EAAE,MAAM,MAAM,EAAE,IAAI,EAAA,CACtD,QAAQ,WAAW,CAAC,OAAO,mBAAmB,kBAAkB,EAAE,IAAI;IAC1G,IAAI,eAAe,KAAK,IAAI,EAAE,IAAI;IAClC,IAAI,CAAC,cAAc,KAAK,IAAI,EAAE,MAAO,+BAAe,IAAI,IAAI,CAAE;IAC9D,MAAM,YAAY,EAAE,UAAU,KAAK,iBAAiB,IAAI,EAAE,IAAI,IAAI,KAAA;IAClE,KAAK,MAAM,UAAU,oBAAoB;KAGvC,IAAI,aAAa,IAAI,MAAM,GAAG;KAE9B,IAAI,aAAa,UAAU,IAAI,MAAM,GAAG;KACxC,aAAa,IAAI,MAAM;KACvB,QAAQ,KAAK;MAAE;MAAG;KAAO,CAAC;IAC5B;IACA,OAAO;KAAE;KAAG;KAAM,UAAU;IAAmB;GACjD,CAAC,CACH;GACA,KAAK,OAAO,SAAS,OAAO,QAAQ;GACpC,KAAK,OAAO,gBAAgB,KAAK,IAAI,OAAO;GAG5C,MAAM,YAAY,KAAK,OAAO,IAAI;GAClC,MAAM,UAAU,MAAM,0BAA0B,QAAQ,WAAW,QAAQ,KAAK,MAAM,KAAKC,UAAU,CAAC,CAAC,CAAC,CAAC;GACzG,MAAM,UAAU,KAAK,OAAO,IAAI;GAChC,KAAK,OAAO,cAAc,KAAK,IAAI,UAAU,SAAS;GAGtD,MAAM,4BAA8C,CAAC;GACrD,MAAM,WAAkB,CAAC;GACzB,QAAQ,SAAS,QAAQ,MAAM;IAC7B,IAAI,OAAO,WAAW,YAAY;KAEhC,IAAI,OAAO,kBAAkB,WAC3B,0BAA0B,KAAK,QAAQ,EAAE;UACpC,IAAI,OAAO,kBAAkB,WAClC,KAAK,oBAAoB,QAAQ,EAAE;UAEnC,SAAS,KAAK,OAAO,MAAM;IAE/B;GACF,CAAC;GAID,IAAI,0BAA0B,SAAS,GAAG;IACxC,MAAM,KAAK,GAAG,aAAa;IAC3B,KAAK,OAAO,gBAAgB,KAAK,IAAI,yBAAyB;IAC9D,MAAM,YAAY,KAAK,OAAO,IAAI;IAClC,MAAM,UAAU,MAAM,0BACpB,QAAQ,WAAW,0BAA0B,KAAK,MAAM,KAAKA,UAAU,CAAC,CAAC,CAAC,CAC5E;IACA,MAAM,UAAU,KAAK,OAAO,IAAI;IAChC,KAAK,OAAO,cAAc,KAAK,IAAI,UAAU,SAAS;IACtD,QAAQ,SAAS,QAAQ,MAAM;KAC7B,IAAI,OAAO,WAAW,YAAY;MAChC,IAAI,OAAO,kBAAkB,WAC3B,KAAK,oBAAoB,0BAA0B,EAAE;WAErD,SAAS,KAAK,OAAO,MAAM;KAE/B;IACF,CAAC;GACH;GAEA,IAAI,SAAS,SAAS,GAAG,MAAM,SAAS;GAExC,KAAK,MAAM,UAAU,SACnB,IAAI,OAAO,EAAE,SAAS;IACpB,IAAI,MAAM,KAAK,iBAAiB,IAAI,OAAO,EAAE,IAAI;IACjD,IAAI,CAAC,KAAK,KAAK,iBAAiB,IAAI,OAAO,EAAE,MAAO,sBAAM,IAAI,IAAI,CAAE;IACpE,IAAI,IAAI,OAAO,MAAM;GACvB;GAKF,IAAI,UAAU,IACZ,MAAM,IAAI,MAAM,8DAA8D;EAElF;CACF;CAEA,MAAM,0BAA0B;EAC9B,MAAM,UAA4B,CAAC;EACnC,KAAK,MAAM,YAAY,KAAK,yBAAyB,OAAO,GAC1D,KAAK,MAAM,UAAU,SAAS,OAAO,GAAG,QAAQ,KAAK,MAAM;EAE7D,KAAK,yBAAyB,MAAM;EAEpC,MAAM,YAAY,KAAK,OAAO,IAAI;EAClC,MAAM,UAAU,MAAM,0BACpB,QAAQ,WAAW,QAAQ,QAAQ,MAAM,CAAC,EAAE,OAAO,eAAe,CAAC,CAAC,KAAK,MAAM,KAAKA,UAAU,CAAC,CAAC,CAAC,CACnG;EACA,MAAM,UAAU,KAAK,OAAO,IAAI;EAChC,KAAK,OAAO,cAAc,KAAK,IAAI,UAAU,SAAS;EAEtD,MAAM,WAAkB,CAAC;EACzB,QAAQ,SAAS,WAAW;GAC1B,IAAI,OAAO,WAAW,YAAY;IAChC,IAAI,OAAO,kBAAkB,WAE3B,KAAK,qBAAqB,KAAK,OAAO,MAAM;SAE5C,SAAS,KAAK,OAAO,MAAM;GAE/B;EACF,CAAC;EACD,IAAI,SAAS,SAAS,GAAG,MAAM,SAAS;CAC1C;CAEA,IAAI,uBAAgC;EAClC,OAAO,KAAK,yBAAyB,OAAO;CAC9C;;CAGA,QAAc;EACZ,KAAK,oCAAoB,IAAI,IAAI;EACjC,KAAK,iBAAiB,MAAM;CAC9B;CAEA,UAAU,QAA0C;EAClD,KAAK,SAAS,UAAU;CAC1B;CAEA,4BAAkC;EAChC,KAAK,uBAAuB,CAAC;CAC/B;CAEA,iCAAuC;EACrC,IAAI,KAAK,qBAAqB,SAAS,GACrC,MAAM,KAAK,qBAAqB;CAEpC;CAEA,UAAU,QAAwB;EAChC,MAAM,EAAE,GAAG,WAAW;EACtB,OAAO,EAAE,SAAS,aAAa,EAAE,GAAG,QAAQ,KAAK,GAAG,GAAG,IAAK,OAAe,EAAE,KAAK,CAAC,KAAK;CAC1F;;CAGA,oBAA4B,QAA8B;EACxD,IAAI,WAAW,KAAK,yBAAyB,IAAI,OAAO,EAAE,IAAI;EAC9D,IAAI,CAAC,UAAU,KAAK,yBAAyB,IAAI,OAAO,EAAE,MAAO,2BAAW,IAAI,IAAI,CAAE;EACtF,SAAS,IAAI,OAAO,QAAQ,MAAM;CACpC;CAEA,YAAY,MAAiD;EAC3D,OAAO,SAAS,eAAe,KAAKD,aAAa,YAAY,KAAKA,aAAa,WAAW,KAAKA,aAAa;CAC9G;CAEA,WAAmB,GAAwD;EACzE,IAAI,UAAU,KAAK,kBAAkB,IAAI,CAAC;EAC1C,IAAI,CAAC,SAAS;GACZ,UAAU;IAAE,sBAAM,IAAI,IAAI;IAAG,sBAAM,IAAI,IAAI;GAAE;GAC7C,KAAK,kBAAkB,IAAI,GAAG,OAAO;GAGrC,KAAK,eAAe,YAAY,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI;EACrD;EACA,OAAO;CACT;CAEA,eAAuB,MAAmC;EACxD,IAAI,QAAQ,KAAK,YAAY,IAAI,KAAK,OAAO;EAC7C,IAAI,CAAC,OAAO;GACV,wBAAQ,IAAI,IAAI;GAChB,KAAK,YAAY,IAAI,KAAK,SAAS,KAAK;EAC1C;EACA,OAAO;CACT;CAEA,cAAsB,QAA6B;EAGjD,MAAM,OAAO,YAAY,MAAM;EAC/B,OAAO,iBAAiB,KAAK,EAAE,CAAC,CAAC,UAAU,MAAM,IAAI,KAAK,8BAA+B,KAAK;CAChG;CAEA,qBAA6B,QAAgB,WAAyC;EACpF,MAAM,OAAO,YAAY,MAAM;EAK/B,QAJgB,iBAAiB,KAAK,EAAE,CAAC,CAAC,UAAU,MAAM,IACtD,KAAK,qCACL,KAAK,kBAAA,CAEM,IAAI,SAAS,KAAK;CACnC;AACF;;AAGA,MAAM,eAAqC,OAAO,OAAO,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"ReactionsManager.js","names":["#needsRecalc","#doAction"],"sources":["../../src/reactivity/ReactionsManager.ts"],"sourcesContent":["import type { Reactable } from \"src/config.ts\";\nimport type { Entity } from \"src/Entity.ts\";\nimport { type EntityManager, getEmInternalApi } from \"src/EntityManager.ts\";\nimport { type EntityMetadata, getMetadata } from \"src/EntityMetadata.ts\";\nimport { NoIdError } from \"src/index.ts\";\nimport { type ReactionLogger, globalLogger, noopReactionLogger } from \"src/logging/ReactionLogger.ts\";\nimport { followReverseHint } from \"src/reactivity/reactiveHints.ts\";\nimport { runInTrustedContext } from \"src/trusted.ts\";\n\nexport type ReactiveAction = { r: Reactable; entity: Entity };\n/**\n * Manages the reactivity of tracking which source fields have changed and finding/recalculating\n * their downstream derived fields.\n *\n * Source fields are usually regular/dumb primitives, i.e. `Author.age` is a source field\n * of `Book.isPublic`, but derived fields can themselves be source fields as well.\n */\nexport class ReactionsManager {\n /** Stores all source `Reactables`s that have been marked for later traversal. */\n private pendingReactables: Map<Reactable, { todo: Set<Entity>; done: Set<Entity> }> = new Map();\n /** Failed actions to retry post-hooks, deduped by target field name -> entity (rare error path). */\n private actionsPendingTypeErrors: Map<string, Map<Entity, ReactiveAction>> = new Map();\n /**\n * A map of entity tagName -> fields that have been marked as dirty.\n *\n * The key of the map is \"just\" the tag name, instead of a specific entity, b/c at the time of\n * being marked dirty, we only have the upstream/source entity + fieldName that has changed, and\n * not the 1-or-more downstream/target entities that will actually recalc.\n *\n * Instead, we just track the dirty fields by type-of-entity, which is enough for `isPendingRecalc`.\n */\n private dirtyFields: Map<string, Set<string>> = new Map();\n /**\n * Tracks which entities have already run a `runOnce` reactable, keyed by the target field\n * name -> entities. We key on `r.name` (not `r` itself) b/c two `Reactable`s reverse-indexed\n * from different source paths can target the same derived field, and must dedupe together.\n * Keying the outer map by field name (of which there are few) instead of by entity (of which\n * there are many) means we allocate just one `Set` per distinct field name, rather than a\n * `Set` per entity or the old per-action `${entity.toTaggedString()}_${r.name}` key strings.\n */\n private processedActions: Map<string, Set<Entity>> = new Map();\n #needsRecalc = { populate: false, query: false, reaction: false };\n // Accessible for EntityManager.runValidation to reuse\n logger: ReactionLogger = globalLogger ?? noopReactionLogger;\n private em: EntityManager;\n /** These are NPEs that *might* have been from invalid m2o fields, so we only throw them after validation. */\n private suppressedTypeErrors: Error[] = [];\n\n constructor(em: EntityManager) {\n this.em = em;\n }\n\n /**\n * Queue all downstream reactive fields that depend on `fieldName` as a source field.\n *\n * This method can be synchronous b/c we internally queue the `Reactable` reverse\n * indexes on the given source `fieldName`, and don't crawl/walk back to the downstream\n * entities/fields until `recalcPendingReactables` is called.\n */\n queueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n // We always queue the reactable/entity, even if we're mid-flush or even mid-recalc, to avoid:\n // - firstName is changed from a1 to a2\n // - this triggers firstName's reactable to `Author.initials` to be queued\n // - during the 1st em.flush loop, we recalc `Author.initials` and it didn't change\n // - during the 1st em.flush loop, a hook changes firstName from a2 to b2\n // - if we skip re-queuing firstName's reactables, we will miss that initials needs\n // its `.load()` called again so that it's `setField` marks `initials` as\n // dirty, otherwise it will be left out of any INSERTs/UPDATEs.\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueued(entity, fieldName, r);\n }\n }\n\n /** Dequeues reactivity on `fieldName`, i.e. if it's no longer dirty. */\n dequeueDownstreamReactables(entity: Entity, fieldName: string): void {\n // Use the reverse index of Reactables that configureMetadata sets up\n for (const r of this.getReactablesByField(entity, fieldName)) {\n const pending = this.getPending(r);\n if (pending.done.has(entity)) {\n // Ironically, if we've already run this reactable, asking to dequeue probably means\n // we need to run it again (to recalc its value), b/c this could be a mid-flush change, i.e.:\n // - firstName = a1 from db\n // - firstName is changed to a2, triggers initials reactable\n // - firstName is changed back to a1, which is the original value, so setField\n // thinks we can dequeue the reactable\n // - but actually our reactable needs to be re-run with the restored value\n pending.todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n } else if (r.fields.length === 1) {\n // We can only delete/dequeue a reaction if `fieldName` is the only or last field\n // that had triggered `r` to run. Since we don't track that currently, i.e. we'd\n // need to have a `Pending.dirtyFields`, for now just only dequeue if `r` only\n // has one field (which is us) anyway.\n pending.todo.delete(entity);\n }\n }\n }\n\n /** Queue all downstream reactive fields that depend on this entity being created or deleted. */\n queueAllDownstreamFields(entity: Entity, reason: \"created\" | \"deleted\"): void {\n for (const r of this.getReactables(entity)) {\n this.getPending(r).todo.add(entity);\n this.#needsRecalc[r.kind] = true;\n this.logger.logQueuedAll(entity, reason, r);\n }\n }\n\n /**\n * Returns whether this field might be pending recalc.\n *\n * This is technically a guess, b/c our reaction infra may not yet have crawled from an upstream\n * source field down to the `N` specific target entities that need recalced, and instead just\n * knows \"it will need to do that soon\" i.e. at the next `em.flush`.\n *\n * So, instead this is a heuristic that reports if `fieldName` has been marked dirty for _some_\n * entities of type `entity`, but we don't technically know if that's _this_ entity.\n *\n * I.e. this might return false positives, but should never return false negatives.\n */\n isMaybePendingRecalc(entity: Entity, fieldName: string): boolean {\n return this.getDirtyFields(getMetadata(entity)).has(fieldName);\n }\n\n hasPendingReactiveQueries(): boolean {\n return this.#needsRecalc.query;\n }\n\n /**\n * Given source `Reactable` \"reverse indexes\" that have been queued as dirty by calls\n * to setters, `em.register`, or `em.delete`, asynchronously walks/crawls to the downstream\n * derived fields and calls `.load()` on them to calc.\n *\n * We also do this in a loop to handle reactive fields depending on other reactive fields.\n */\n async recalcPendingReactables(kind: \"reactables\" | \"reactiveQueries\") {\n if (this.needsRecalc(kind)) this.logger.logStartingRecalc(this.em, kind);\n\n let loops = 0;\n while (this.needsRecalc(kind)) {\n // ...we probably should only loop for `kind=reactables` Reactables, and `kind=reactiveQueries`\n // AsyncReactiveFields should probably only have a single loop, after which we return and\n // let `em.flush` push the latest values to the db, so our 2nd-order AsyncReactiveFields\n // will see the latest value.\n if (kind === \"reactables\") {\n this.#needsRecalc.populate = false;\n this.#needsRecalc.reaction = false;\n } else {\n this.#needsRecalc.query = false;\n }\n\n // Dedupe actions by (target field name, entity); see the `processedActions` docs for why\n // we key on `r.name` instead of `r` or the old per-action key strings\n const seen: Map<string, Set<Entity>> = new Map();\n const actions: ReactiveAction[] = [];\n\n const walks = await Promise.all(\n [...this.pendingReactables.entries()].map(async ([r, pending]) => {\n // Skip reactive queries until post-flush\n if (kind === \"reactables\" && r.kind === \"query\") return undefined;\n if (kind === \"reactiveQueries\" && (r.kind === \"populate\" || r.kind === \"reaction\")) return undefined;\n // Copy pending and clear it\n const todo = [...pending.todo];\n if (todo.length === 0) return undefined;\n pending.todo.clear();\n for (const doing of todo) pending.done.add(doing);\n // Walk back from the source to any downstream entities\n const entities = r.path.length === 0 ? todo : await followReverseHint(r.name, todo, r.path);\n const actionableEntities = entities.filter((entity) => !entity.isDeletedEntity && entity instanceof r.cstr);\n let seenEntities = seen.get(r.name);\n if (!seenEntities) seen.set(r.name, (seenEntities = new Set()));\n const processed = r.runOnce ? this.processedActions.get(r.name) : undefined;\n for (const entity of actionableEntities) {\n // We could arrive at the same reactable from multiple paths (eg, 2 dependent fields changed), so we need to\n // dedupe based on the entity and reactable to only run each action once for any given entity per loop\n if (seenEntities.has(entity)) continue;\n // If this reactable has already run and shouldn't run again, then skip it\n if (processed && processed.has(entity)) continue;\n seenEntities.add(entity);\n actions.push({ r, entity });\n }\n return { r, todo, entities: actionableEntities };\n }),\n );\n this.logger.logWalks(walks, \"recalc\");\n this.logger.logLoadingStart(this.em, actions);\n // Use allSettled so that we can watch for derived values that want to use an entity's id\n // i.e. they can fail, but we'll queue them from later.\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() => Promise.allSettled(actions.map((a) => this.#doAction(a))));\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n // This loop's actions are already unique by (name, entity), so a plain array dedupes fine\n const actionsPendingAssignedIds: ReactiveAction[] = [];\n const failures: any[] = [];\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n // Let `author.id` and `book.author.get.firstName` errors run again after flush/hooks fills them in\n if (result.reason instanceof NoIdError) {\n actionsPendingAssignedIds.push(actions[i]);\n } else if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actions[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n\n // If we have any actions that need to be re-run because they failed due to a missing id, then we assign ids and\n // re-run them.\n if (actionsPendingAssignedIds.length > 0) {\n await this.em.assignNewIds();\n this.logger.logLoadingStart(this.em, actionsPendingAssignedIds);\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actionsPendingAssignedIds.map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n results.forEach((result, i) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n this.addPendingTypeError(actionsPendingAssignedIds[i]);\n } else {\n failures.push(result.reason);\n }\n }\n });\n }\n\n if (failures.length > 0) throw failures[0];\n // Record any successful actions that should only run once so we don't run them again\n for (const action of actions) {\n if (action.r.runOnce) {\n let set = this.processedActions.get(action.r.name);\n if (!set) this.processedActions.set(action.r.name, (set = new Set()));\n set.add(action.entity);\n }\n }\n // This should generally not happen, only if two reactive fields depend on each other,\n // which in theory should probably be caught/blow up in the `configureMetadata` step,\n // but if it's not caught sooner, at least don't infinite loop.\n if (loops++ > 50) {\n throw new Error(\"recalc looped too many times, probably a circular dependency\");\n }\n }\n }\n\n async recalcPendingTypeErrors() {\n const actions: ReactiveAction[] = [];\n for (const byEntity of this.actionsPendingTypeErrors.values()) {\n for (const action of byEntity.values()) actions.push(action);\n }\n this.actionsPendingTypeErrors.clear();\n\n const startTime = this.logger.now();\n const results = await runInTrustedContext(() =>\n Promise.allSettled(actions.filter((a) => !a.entity.isDeletedEntity).map((a) => this.#doAction(a))),\n );\n const endTime = this.logger.now();\n this.logger.logLoadingEnd(this.em, endTime - startTime);\n\n const failures: any[] = [];\n results.forEach((result) => {\n if (result.status === \"rejected\") {\n if (result.reason instanceof TypeError) {\n // Defer to the validation error to catch this\n this.suppressedTypeErrors.push(result.reason);\n } else {\n failures.push(result.reason);\n }\n }\n });\n if (failures.length > 0) throw failures[0];\n }\n\n get hasPendingTypeErrors(): boolean {\n return this.actionsPendingTypeErrors.size > 0;\n }\n\n /** Clears all the pending source fields, i.e. after `em.flush` is complete. */\n clear(): void {\n this.pendingReactables = new Map();\n this.processedActions.clear();\n }\n\n setLogger(logger: ReactionLogger | undefined): void {\n this.logger = logger ?? noopReactionLogger;\n }\n\n clearSuppressedTypeErrors(): void {\n this.suppressedTypeErrors = [];\n }\n\n throwIfAnySuppressedTypeErrors(): void {\n if (this.suppressedTypeErrors.length > 0) {\n throw this.suppressedTypeErrors[0];\n }\n }\n\n #doAction(action: ReactiveAction) {\n const { r, entity } = action;\n return r.kind === \"reaction\" ? r.fn(entity, this.em.ctx) : (entity as any)[r.name].load();\n }\n\n /** Remembers a TypeError'd action to retry post-hooks, deduped by (target field name, entity). */\n private addPendingTypeError(action: ReactiveAction): void {\n let byEntity = this.actionsPendingTypeErrors.get(action.r.name);\n if (!byEntity) this.actionsPendingTypeErrors.set(action.r.name, (byEntity = new Map()));\n byEntity.set(action.entity, action);\n }\n\n needsRecalc(kind: \"reactables\" | \"reactiveQueries\"): boolean {\n return kind === \"reactables\" ? this.#needsRecalc.populate || this.#needsRecalc.reaction : this.#needsRecalc.query;\n }\n\n private getPending(r: Reactable): { todo: Set<Entity>; done: Set<Entity> } {\n let pending = this.pendingReactables.get(r);\n if (!pending) {\n pending = { todo: new Set(), done: new Set() };\n this.pendingReactables.set(r, pending);\n // The dirty field name is constant per reactable, so mark it once on the first\n // queue instead of paying the `getMetadata` + map lookup on every queued entity.\n this.getDirtyFields(getMetadata(r.cstr)).add(r.name);\n }\n return pending;\n }\n\n private getDirtyFields(meta: EntityMetadata): Set<string> {\n let dirty = this.dirtyFields.get(meta.tagName);\n if (!dirty) {\n dirty = new Set();\n this.dirtyFields.set(meta.tagName, dirty);\n }\n return dirty;\n }\n\n private getReactables(entity: Entity): Reactable[] {\n // If two books are getting merged, and so a normally-immutable `BookReview.book` is being changed,\n // then even normally-immutable fields need to be recalculated.\n const meta = getMetadata(entity);\n return getEmInternalApi(this.em).isMerging(entity) ? meta.reactablesIncludingReadOnly! : meta.reactables!;\n }\n\n private getReactablesByField(entity: Entity, fieldName: string): readonly Reactable[] {\n const meta = getMetadata(entity);\n const byField = getEmInternalApi(this.em).isMerging(entity)\n ? meta.reactablesIncludingReadOnlyByField!\n : meta.reactablesByField!;\n // Most fields have no reactables, so return a shared empty array instead of allocating one per set\n return byField.get(fieldName) ?? noReactables;\n }\n}\n\n/** A shared frozen array for fields with no downstream reactables. */\nconst noReactables: readonly Reactable[] = Object.freeze([]);\n"],"mappings":"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"}
|