joist-test-utils 2.4.0-next.3 → 2.4.0-next.31

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.
@@ -15,7 +15,7 @@ var RunPlugin = class extends joist_core.Plugin {
15
15
  api.clearPreloadedRelations();
16
16
  api.clearDataloaders();
17
17
  this.#syncEntityData(entityTodos);
18
- this.#syncReferences(entityTodos);
18
+ this.#syncReferencesAndLazyFields(entityTodos);
19
19
  this.#syncManyToManys(joinRowTodos);
20
20
  this.#syncEnumCollections(joinRowTodos);
21
21
  this.#preloadUnloadedRelations(entityTodos);
@@ -52,11 +52,12 @@ var RunPlugin = class extends joist_core.Plugin {
52
52
  });
53
53
  });
54
54
  }
55
- #syncReferences(entityTodos) {
55
+ #syncReferencesAndLazyFields(entityTodos) {
56
56
  const { em } = this;
57
57
  Object.values(entityTodos).forEach((todo) => {
58
58
  todo.inserts.forEach((newEntity) => {
59
59
  const oldEntity = em.findExistingInstance(newEntity.idTagged);
60
+ for (const field of getLazyFields(newEntity)) preloadLazyField(oldEntity, field.fieldName);
60
61
  getReferences(newEntity).forEach((r) => {
61
62
  const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : void 0;
62
63
  preloadReference(em, r.fieldName, oldEntity, other);
@@ -65,6 +66,7 @@ var RunPlugin = class extends joist_core.Plugin {
65
66
  });
66
67
  todo.updates.forEach((newEntity) => {
67
68
  const oldEntity = em.findExistingInstance(newEntity.idTagged);
69
+ for (const field of getLazyFields(newEntity)) if (newEntity.changes[field.fieldName].hasChanged) preloadLazyField(oldEntity, field.fieldName);
68
70
  getReferences(newEntity).filter((r) => newEntity.changes[r.fieldName].hasChanged).forEach((r) => {
69
71
  const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : void 0;
70
72
  preloadReference(em, r.fieldName, oldEntity, other);
@@ -150,6 +152,10 @@ var RunPlugin = class extends joist_core.Plugin {
150
152
  });
151
153
  }
152
154
  };
155
+ /** Preloads a mirrored lazy value without treating the database write as a test-entity mutation. */
156
+ function preloadLazyField(entity, fieldName) {
157
+ entity[fieldName].preload();
158
+ }
153
159
  function preloadReference(em, fieldName, entity, other) {
154
160
  const reference = entity[fieldName];
155
161
  reference.unload();
@@ -204,6 +210,10 @@ function processJoinRows(em, joinTable, rows, preloads, callback) {
204
210
  });
205
211
  });
206
212
  }
213
+ /** Returns the lazy columns on an entity, including inherited fields. */
214
+ function getLazyFields(entity) {
215
+ return Object.values((0, joist_core.getMetadata)(entity).allFields).values().filter((field) => field.kind === "primitive").filter((field) => !!field.lazy);
216
+ }
207
217
  function getReferences(entity) {
208
218
  return (0, joist_core.getRelations)(entity).values().filter((r) => r instanceof joist_core.ManyToOneReferenceImpl || r instanceof joist_core.ReactiveReferenceImpl || r instanceof joist_core.PolymorphicReferenceImpl);
209
219
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RunPlugin.cjs","names":["Plugin","getEmInternalApi","#syncEntityData","#syncReferences","#syncManyToManys","#syncEnumCollections","#preloadUnloadedRelations","createRowFromEntityData","getInstanceData","getMetadata","PolymorphicKeySerde","PojoRowData","OneToManyCollection","isEntity","getRelations","OneToOneReferenceImpl","ManyToOneReferenceImpl","ReactiveReferenceImpl","PolymorphicReferenceImpl","ManyToManyCollection","ReactiveManyToManyImpl","ReactiveManyToManyOtherSideImpl","EnumCollectionImpl"],"sources":["../src/RunPlugin.ts"],"sourcesContent":["import {\n type Entity,\n type EntityManager,\n EnumCollectionImpl,\n type FieldColumn,\n type JoinRow,\n type JoinRowTodo,\n ManyToManyCollection,\n ManyToOneReferenceImpl,\n type MaybeAbstractEntityConstructor,\n OneToManyCollection,\n OneToOneReferenceImpl,\n Plugin,\n PojoRowData,\n PolymorphicKeySerde,\n PolymorphicReferenceImpl,\n ReactiveManyToManyImpl,\n ReactiveManyToManyOtherSideImpl,\n ReactiveReferenceImpl,\n type Todo,\n assertNever,\n createRowFromEntityData,\n getEmInternalApi,\n getInstanceData,\n getMetadata,\n getRelations,\n isEntity,\n} from \"joist-core\";\n\n/*\n * `run...` helpers use this plugin to mirror any changes made to their isolated entity manager inside to the original\n * entity manager they were passed. This allows changes to be synchronously reflected between the two entity managers\n * without needing to query the database.\n */\nexport class RunPlugin extends Plugin {\n // The em passed here is the target which we'll mirror changes to. The plugin should not be added to it, but rather\n // to the new em `run..` helpers create or by downstream ems created by implementation code\n constructor(public readonly em: EntityManager) {\n super();\n }\n\n maybeCloneForNewEm() {\n return this;\n }\n\n afterWrite(entityTodos: Record<string, Todo>, joinRowTodos: Record<string, JoinRowTodo>) {\n const api = getEmInternalApi(this.em);\n // We don't want any old preloaded data leaking into our changes, so we clear it out before we start as a precaution\n api.clearPreloadedRelations();\n // Likewise, any data loaders in memory will have stale data, so we clear them out as well\n api.clearDataloaders();\n this.#syncEntityData(entityTodos);\n this.#syncReferences(entityTodos);\n this.#syncManyToManys(joinRowTodos);\n this.#syncEnumCollections(joinRowTodos);\n this.#preloadUnloadedRelations(entityTodos);\n }\n\n #syncEntityData(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const row = createRowFromEntityData(newEntity, { preferOriginalData: false });\n em.hydrate(newEntity.constructor as MaybeAbstractEntityConstructor<any>, [row]);\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n // This fail shouldn't really be possible, since any entity should either have been created:\n // * via factory or em.create in the original em\n // * by the new em inside the `run` call, in which case it'd be an insert instead of an update\n // * by a previous afterWrite call, which would have already been an insert and added to the original em\n // * by a previous `run` call, which would have already been an insert as well\n if (!oldEntity) fail(`Expected to find existing entity for $ {e.idTagged}`);\n const { data: newData } = getInstanceData(newEntity);\n const { data: oldData } = getInstanceData(oldEntity);\n const meta = getMetadata(newEntity);\n (newEntity as any).changes.fields\n .values()\n .filter((fieldName: string) => fieldName in newData)\n .filter((fieldName: string) => !!meta.allFields[fieldName].serde)\n .forEach((fieldName: string) => {\n // We're imitating a round trip to the database here, so we use our field's serde to map the value back\n // and forth.\n const serde = meta.allFields[fieldName].serde!;\n let column: FieldColumn | undefined;\n let value: any;\n if (serde instanceof PolymorphicKeySerde) {\n [column, value] =\n serde.columns\n .values()\n .map((c) => [c, c.rowValue(newData)] as const)\n .find(([, v]) => v !== undefined) ?? [];\n } else {\n [column] = serde.columns;\n // the 2nd and 3rd arguments are only used if the 4th argument is defined, so it's OK to pass undefined here\n value = column.rowValue(newData);\n }\n oldData[fieldName] = serde.fromRow(new PojoRowData([column ? { [column.columnName]: value } : {}]), 0);\n });\n });\n todo.deletes.forEach((newEntity) => {\n const existing = em.findExistingInstance(newEntity.idTagged);\n // Again it shouldn't really be possible for a deletion to happen without the entity being present in the\n // original em, but just in case we'll skip if we don't find it\n if (!existing) fail(`Expected to find existing entity for $ {e.idTagged}`);\n getInstanceData(existing).markDeletedBecauseNotFound();\n });\n });\n }\n\n // Ensure m2o/rr/poly changes from the new em are reflected in the original em\n #syncReferences(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n });\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity)\n .filter((r) => (newEntity as any).changes[r.fieldName].hasChanged)\n .forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n const originalId = (newEntity as any).changes[r.fieldName].originalValue;\n const originalOther = originalId ? em.findExistingInstance(originalId) : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n // If the entity was soft-deleted, then we need to clear out the caches for any o2m collections that contain it\n if (\"deletedAt\" in newEntity && (newEntity as any).changes.deletedAt.hasChanged) {\n getReferences(oldEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n if (other) {\n const otherSide = getOtherSide(r.fieldName, oldEntity, other);\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n }\n });\n }\n });\n todo.deletes.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n // Since both old and new entities are marked as deleted, we can't just go looking through their relations\n // for the other side's id since this will error. Likewise, we only care about the collection that this\n // entity used to belong to in the old em so we can remove it. So just grab the other id directly from the\n // old entity's data and then preload its other side relation.\n const entityOrId = getInstanceData(oldEntity).data[r.fieldName];\n const originalOther = entityOrId\n ? em.findExistingInstance(isEntity(entityOrId) ? entityOrId.idTagged : entityOrId)\n : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n });\n });\n }\n\n // Ensure m2m changes are propagated from the new em to the original em\n #syncManyToManys(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n Object.entries(joinRowTodos).forEach(([joinTable, todo]) => {\n // Enum m2ms (EnumCollections) aren't entity-to-entity, so there's nothing to mirror between ems.\n if (todo.m2m.otherEnum) return;\n const preloads = new Set<ManyToManyCollection<Entity, Entity>>();\n processJoinRows(em, joinTable, todo.newRows, preloads, (oldEntity, entities) => entities.push(oldEntity));\n processJoinRows(em, joinTable, todo.deletedRows, preloads, (oldEntity, entities) =>\n entities.splice(entities.indexOf(oldEntity), 1),\n );\n // Since multiple join rows could reference the same m2m, our processJoinRows doesn't actually call preload() so\n // we need to do it now\n preloads.values().forEach((r) => {\n const id = r.entity.idTagged;\n const entities = api.getPreloadedRelation(id, r.fieldName) as Entity[];\n // unique our entities in case somehow an entity was inserted by multiple join rows\n api.setPreloadedRelation(id, r.fieldName, [...new Set(entities)]);\n r.preload();\n });\n });\n }\n\n // Ensure enum m2m (EnumCollection) membership changes are propagated from the new em to the original em\n #syncEnumCollections(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n const touched = new Set<EnumCollectionImpl<Entity, any>>();\n Object.values(joinRowTodos).forEach((todo) => {\n // Only enum m2ms; entity-to-entity m2ms are handled by `#syncManyToManys`.\n if (!todo.m2m.otherEnum) return;\n const { columnName, otherColumnName, fieldName } = todo.m2m;\n const sync = (rows: JoinRow[], add: boolean) => {\n rows.forEach((row) => {\n const newEntity = row.columns[columnName] as Entity;\n const enumId = row.columns[otherColumnName] as number;\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n if (!oldEntity || oldEntity.isDeletedEntity) return;\n const oldCollection = (oldEntity as any)[fieldName] as EnumCollectionImpl<Entity, any>;\n // Only mirror collections the original em has loaded (or new entities, whose rows are all\n // adds); otherwise a later `.load()` will read the (same-txn) db and see the change itself.\n if (!oldCollection.isLoaded && !newEntity.isNewEntity) return;\n const joinRows = api.joinRows(oldCollection);\n if (add) {\n joinRows.addPreloadedRow(oldCollection, undefined, oldEntity, enumId);\n } else {\n joinRows.removePreloadedRow(oldCollection, oldEntity, enumId);\n }\n touched.add(oldCollection);\n });\n };\n sync(todo.newRows, true);\n sync(todo.deletedRows, false);\n });\n // Re-publish each touched collection's codes and mark it loaded.\n touched.forEach((oldCollection) => {\n const joinRows = api.joinRows(oldCollection);\n const codes = (joinRows.getOthers(oldCollection.columnName, oldCollection.entity) as number[]).map(\n (id) => oldCollection.otherEnum!.findById(id)!.code,\n );\n api.setPreloadedRelation(oldCollection.entity.idTagged, oldCollection.fieldName, codes);\n oldCollection.preload();\n });\n }\n\n // Make sure any unloaded relations on net new entities are marked as loaded. We should have already preloaded all\n // concrete references in a previous step. So we just need to load collections (plus o2os) if they haven't already\n // been preloaded by another step. Since nothing else preloaded them, they must have no data so we can just set\n // them to an empty array.\n #preloadUnloadedRelations(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.entries(entityTodos).forEach(([, todo]) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getCollections(oldEntity)\n .filter((r) => !r.isPreloaded)\n .forEach((r) => {\n getEmInternalApi(em).setPreloadedRelation(oldEntity.idTagged, r.fieldName, []);\n r.preload();\n });\n });\n });\n }\n}\n\n// References that represent one (or more) foreign keys directly on the entity\ntype ConcreteReference =\n | ManyToOneReferenceImpl<Entity, Entity, any>\n | ReactiveReferenceImpl<Entity, Entity, any, any>\n | PolymorphicReferenceImpl<Entity, Entity, any>;\n\nfunction preloadReference(em: EntityManager, fieldName: string, entity: Entity, other: Entity | undefined) {\n // Preload for concrete references doesn't make us of the global preloaded cache. It just looks at its owner's data\n // to get a key, then checks if that entity is already in the em and uses that if so. So we just need to set the\n // owner's data to have the correct key then we can call preload since all entities should already be in the original\n // em\n const reference = (entity as any)[fieldName] as ConcreteReference;\n (reference as any).unload();\n const { data } = getInstanceData(entity);\n data[fieldName] = other?.idTagged;\n if (!entity.isDeletedEntity) reference.preload();\n}\n\nfunction getOtherSide(fieldName: string, entity: Entity, other: Entity) {\n const meta = getMetadata(entity);\n const metas = new Set([meta, ...meta.baseTypes]);\n return getRelations(other)\n .values()\n .filter((r) => r instanceof OneToOneReferenceImpl || r instanceof OneToManyCollection)\n .find((r) => metas.has(r.otherMeta) && r.otherFieldName === fieldName);\n}\n\nfunction maybePreloadOtherSide(\n em: EntityManager,\n fieldName: string,\n entity: Entity,\n other: Entity,\n remove: boolean = false,\n): void {\n const meta = getMetadata(entity);\n const otherSide = getOtherSide(fieldName, entity, other);\n // If our other side is a large o2m then we can just ignore it, since it isn't loadable regardless\n if (!otherSide) return;\n // Since our preload scans the entire em, we only need to run it once per collection/o2o. We clear out preloads\n // when we start the afterWrite, so if anything is preloaded we know we put it there.\n if (otherSide.isPreloaded) return;\n let otherEntities: Entity[];\n if (otherSide instanceof OneToOneReferenceImpl) {\n otherEntities = remove ? [] : [entity];\n } else if (otherSide instanceof OneToManyCollection) {\n // Scan the entire em for (non-deleted) entities of our type that reference the other side\n otherEntities = em\n .getEntities(meta.cstr)\n .values()\n .filter((e) => !e.isDeletedEntity)\n .filter((e) => ((e as any)[otherSide.otherFieldName] as ConcreteReference).idTaggedMaybe === other.idTagged)\n .toArray();\n } else {\n assertNever(otherSide);\n }\n // We don't need to worry about order here because o2ms do their own sorting internally and o2os are inherently\n // unordered (since it's just a single entity)\n getEmInternalApi(em).setPreloadedRelation(other.idTagged, otherSide.fieldName, otherEntities);\n otherSide.preload();\n // This method is confusingly name, what it actually does is clear out the sorted caches that m2m uses to avoid\n // sorting on every `.get` call. Since we just preloaded the collection with new data, we know these caches need to\n // be cleared\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n}\n\nfunction processJoinRows(\n em: EntityManager,\n joinTable: string,\n rows: JoinRow[],\n preloads: Set<ManyToManyCollection<Entity, Entity>>,\n callback: (oldEntity: Entity, entities: Entity[]) => void,\n) {\n const api = getEmInternalApi(em);\n rows.forEach((row) => {\n // A join row's `columns` is always an object with two key/value pairs, one for each side of the m2m.\n // This only runs for entity-to-entity m2ms (enum m2ms are skipped by the caller), so both are entities.\n const [[col1, e1], [col2, e2]] = Object.entries(row.columns) as [[string, Entity], [string, Entity]];\n (\n [\n // Each join row needs to append/remove from both sides of the m2m. So we need to do the same operation once for\n // each side.\n [col1, e1, e2],\n [col2, e2, e1],\n ] as const\n ).forEach(([column, newEntity, newOther]) => {\n const { idTagged: id } = newEntity;\n const oldEntity = em.findExistingInstance(id)!;\n if (oldEntity.isDeletedEntity) return;\n const newM2m = getManyToManys(newEntity).find((r) => r.joinTableName === joinTable && r.columnName === column)!;\n const { fieldName } = newM2m;\n const oldM2m = (oldEntity as any)[fieldName] as ManyToManyCollection<Entity, Entity>;\n let entities: Entity[];\n if (preloads.has(oldM2m)) {\n // If a previous join row has already preloaded this relation, then we can just append/remove from the\n // existing preloaded data.\n entities = api.getPreloadedRelation(id, fieldName) as Entity[];\n } else if (oldM2m.isLoaded) {\n // The order here is important. M2ms are returned in the order their join rows were inserted, so we need to\n // make sure we match that behavior here. So we get all the extant rows first and then append each new row\n // in order from there. Removals are fine since they'll modify the array in place and the correct order will\n // be preserved.\n entities = oldM2m.get;\n } else if (newEntity.isNewEntity) {\n // If this is a new entity, then all the join rows referencing it must also be inserts. So we can just use an\n // empty array initially and inserts will be appended to it.\n entities = [];\n } else {\n fail(`Expected ${fieldName} to be loaded`);\n }\n callback(em.findExistingInstance(newOther.idTagged)!, entities);\n api.setPreloadedRelation(id, fieldName, entities);\n // We only need to preload the m2m once, but multiple join rows could reference it, so we keep track of which m2ms\n // need to be preloaded as we work, then actually call preload() at the end once we're done.\n preloads.add(oldM2m);\n });\n });\n}\n\nfunction getReferences(entity: Entity): IteratorObject<ConcreteReference> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToOneReferenceImpl ||\n r instanceof ReactiveReferenceImpl ||\n r instanceof PolymorphicReferenceImpl,\n );\n}\n\nfunction getManyToManys(\n entity: Entity,\n): IteratorObject<\n | ManyToManyCollection<Entity, Entity>\n | ReactiveManyToManyImpl<any, any, any>\n | ReactiveManyToManyOtherSideImpl<any, any>\n> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl,\n );\n}\n\nfunction getCollections(entity: Entity) {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof OneToManyCollection ||\n r instanceof OneToOneReferenceImpl ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl ||\n r instanceof EnumCollectionImpl,\n );\n}\n"],"mappings":";;;AAkCA,IAAa,YAAb,cAA+BA,WAAAA,OAAO;CAGR;CAA5B,YAAY,IAAmC;EAC7C,MAAM;EADoB,KAAA,KAAA;CAE5B;CAEA,qBAAqB;EACnB,OAAO;CACT;CAEA,WAAW,aAAmC,cAA2C;EACvF,MAAM,OAAA,GAAMC,WAAAA,iBAAAA,CAAiB,KAAK,EAAE;EAEpC,IAAI,wBAAwB;EAE5B,IAAI,iBAAiB;EACrB,KAAKC,gBAAgB,WAAW;EAChC,KAAKC,gBAAgB,WAAW;EAChC,KAAKC,iBAAiB,YAAY;EAClC,KAAKC,qBAAqB,YAAY;EACtC,KAAKC,0BAA0B,WAAW;CAC5C;CAEA,gBAAgB,aAAmC;EACjD,MAAM,EAAE,OAAO;EACf,OAAO,OAAO,WAAW,CAAC,CAAC,SAAS,SAAS;GAC3C,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,OAAA,GAAMC,WAAAA,wBAAAA,CAAwB,WAAW,EAAE,oBAAoB,MAAM,CAAC;IAC5E,GAAG,QAAQ,UAAU,aAAoD,CAAC,GAAG,CAAC;GAChF,CAAC;GACD,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;IAM5D,IAAI,CAAC,WAAW,KAAK,qDAAqD;IAC1E,MAAM,EAAE,MAAM,aAAA,GAAYC,WAAAA,gBAAAA,CAAgB,SAAS;IACnD,MAAM,EAAE,MAAM,aAAA,GAAYA,WAAAA,gBAAAA,CAAgB,SAAS;IACnD,MAAM,QAAA,GAAOC,WAAAA,YAAAA,CAAY,SAAS;IAClC,UAAmB,QAAQ,OACxB,OAAO,CAAC,CACR,QAAQ,cAAsB,aAAa,OAAO,CAAC,CACnD,QAAQ,cAAsB,CAAC,CAAC,KAAK,UAAU,UAAU,CAAC,KAAK,CAAC,CAChE,SAAS,cAAsB;KAG9B,MAAM,QAAQ,KAAK,UAAU,UAAU,CAAC;KACxC,IAAI;KACJ,IAAI;KACJ,IAAI,iBAAiBC,WAAAA,qBACnB,CAAC,QAAQ,SACP,MAAM,QACH,OAAO,CAAC,CACR,KAAK,MAAM,CAAC,GAAG,EAAE,SAAS,OAAO,CAAC,CAAU,CAAC,CAC7C,MAAM,GAAG,OAAO,MAAM,KAAA,CAAS,KAAK,CAAC;UACrC;MACL,CAAC,UAAU,MAAM;MAEjB,QAAQ,OAAO,SAAS,OAAO;KACjC;KACA,QAAQ,aAAa,MAAM,QAAQ,IAAIC,WAAAA,YAAY,CAAC,SAAS,GAAG,OAAO,aAAa,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;IACvG,CAAC;GACL,CAAC;GACD,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,WAAW,GAAG,qBAAqB,UAAU,QAAQ;IAG3D,IAAI,CAAC,UAAU,KAAK,qDAAqD;IACzE,CAAA,GAAA,WAAA,gBAAA,CAAgB,QAAQ,CAAC,CAAC,2BAA2B;GACvD,CAAC;EACH,CAAC;CACH;CAGA,gBAAgB,aAAmC;EACjD,MAAM,EAAE,OAAO;EACf,OAAO,OAAO,WAAW,CAAC,CAAC,SAAS,SAAS;GAC3C,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;IAC5D,cAAc,SAAS,CAAC,CAAC,SAAS,MAAM;KACtC,MAAM,QAAQ,EAAE,QAAQ,GAAG,qBAAqB,EAAE,aAAa,IAAI,KAAA;KACnE,iBAAiB,IAAI,EAAE,WAAW,WAAW,KAAK;KAClD,IAAI,OAAO,sBAAsB,IAAI,EAAE,WAAW,WAAW,KAAK;IACpE,CAAC;GACH,CAAC;GACD,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;IAC5D,cAAc,SAAS,CAAC,CACrB,QAAQ,MAAO,UAAkB,QAAQ,EAAE,UAAU,CAAC,UAAU,CAAC,CACjE,SAAS,MAAM;KACd,MAAM,QAAQ,EAAE,QAAQ,GAAG,qBAAqB,EAAE,aAAa,IAAI,KAAA;KACnE,iBAAiB,IAAI,EAAE,WAAW,WAAW,KAAK;KAClD,IAAI,OAAO,sBAAsB,IAAI,EAAE,WAAW,WAAW,KAAK;KAClE,MAAM,aAAc,UAAkB,QAAQ,EAAE,UAAU,CAAC;KAC3D,MAAM,gBAAgB,aAAa,GAAG,qBAAqB,UAAU,IAAI,KAAA;KACzE,IAAI,eAAe,sBAAsB,IAAI,EAAE,WAAW,WAAW,eAAe,IAAI;IAC1F,CAAC;IAEH,IAAI,eAAe,aAAc,UAAkB,QAAQ,UAAU,YACnE,cAAc,SAAS,CAAC,CAAC,SAAS,MAAM;KACtC,MAAM,QAAQ,EAAE,QAAQ,GAAG,qBAAqB,EAAE,aAAa,IAAI,KAAA;KACnE,IAAI,OAAO;MACT,MAAM,YAAY,aAAa,EAAE,WAAW,WAAW,KAAK;MAC5D,IAAI,qBAAqBC,WAAAA,qBAAqB,UAAU,cAAc;KACxE;IACF,CAAC;GAEL,CAAC;GACD,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;IAC5D,cAAc,SAAS,CAAC,CAAC,SAAS,MAAM;KAKtC,MAAM,cAAA,GAAaJ,WAAAA,gBAAAA,CAAgB,SAAS,CAAC,CAAC,KAAK,EAAE;KACrD,MAAM,gBAAgB,aAClB,GAAG,sBAAA,GAAqBK,WAAAA,SAAAA,CAAS,UAAU,IAAI,WAAW,WAAW,UAAU,IAC/E,KAAA;KACJ,IAAI,eAAe,sBAAsB,IAAI,EAAE,WAAW,WAAW,eAAe,IAAI;IAC1F,CAAC;GACH,CAAC;EACH,CAAC;CACH;CAGA,iBAAiB,cAA2C;EAC1D,MAAM,EAAE,OAAO;EACf,MAAM,OAAA,GAAMZ,WAAAA,iBAAAA,CAAiB,EAAE;EAC/B,OAAO,QAAQ,YAAY,CAAC,CAAC,SAAS,CAAC,WAAW,UAAU;GAE1D,IAAI,KAAK,IAAI,WAAW;GACxB,MAAM,2BAAW,IAAI,IAA0C;GAC/D,gBAAgB,IAAI,WAAW,KAAK,SAAS,WAAW,WAAW,aAAa,SAAS,KAAK,SAAS,CAAC;GACxG,gBAAgB,IAAI,WAAW,KAAK,aAAa,WAAW,WAAW,aACrE,SAAS,OAAO,SAAS,QAAQ,SAAS,GAAG,CAAC,CAChD;GAGA,SAAS,OAAO,CAAC,CAAC,SAAS,MAAM;IAC/B,MAAM,KAAK,EAAE,OAAO;IACpB,MAAM,WAAW,IAAI,qBAAqB,IAAI,EAAE,SAAS;IAEzD,IAAI,qBAAqB,IAAI,EAAE,WAAW,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,CAAC;IAChE,EAAE,QAAQ;GACZ,CAAC;EACH,CAAC;CACH;CAGA,qBAAqB,cAA2C;EAC9D,MAAM,EAAE,OAAO;EACf,MAAM,OAAA,GAAMA,WAAAA,iBAAAA,CAAiB,EAAE;EAC/B,MAAM,0BAAU,IAAI,IAAqC;EACzD,OAAO,OAAO,YAAY,CAAC,CAAC,SAAS,SAAS;GAE5C,IAAI,CAAC,KAAK,IAAI,WAAW;GACzB,MAAM,EAAE,YAAY,iBAAiB,cAAc,KAAK;GACxD,MAAM,QAAQ,MAAiB,QAAiB;IAC9C,KAAK,SAAS,QAAQ;KACpB,MAAM,YAAY,IAAI,QAAQ;KAC9B,MAAM,SAAS,IAAI,QAAQ;KAC3B,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;KAC5D,IAAI,CAAC,aAAa,UAAU,iBAAiB;KAC7C,MAAM,gBAAiB,UAAkB;KAGzC,IAAI,CAAC,cAAc,YAAY,CAAC,UAAU,aAAa;KACvD,MAAM,WAAW,IAAI,SAAS,aAAa;KAC3C,IAAI,KACF,SAAS,gBAAgB,eAAe,KAAA,GAAW,WAAW,MAAM;UAEpE,SAAS,mBAAmB,eAAe,WAAW,MAAM;KAE9D,QAAQ,IAAI,aAAa;IAC3B,CAAC;GACH;GACA,KAAK,KAAK,SAAS,IAAI;GACvB,KAAK,KAAK,aAAa,KAAK;EAC9B,CAAC;EAED,QAAQ,SAAS,kBAAkB;GAEjC,MAAM,QADW,IAAI,SAAS,aACR,CAAC,CAAC,UAAU,cAAc,YAAY,cAAc,MAAM,CAAC,CAAc,KAC5F,OAAO,cAAc,UAAW,SAAS,EAAE,CAAC,CAAE,IACjD;GACA,IAAI,qBAAqB,cAAc,OAAO,UAAU,cAAc,WAAW,KAAK;GACtF,cAAc,QAAQ;EACxB,CAAC;CACH;CAMA,0BAA0B,aAAmC;EAC3D,MAAM,EAAE,OAAO;EACf,OAAO,QAAQ,WAAW,CAAC,CAAC,SAAS,GAAG,UAAU;GAChD,KAAK,QAAQ,SAAS,cAAc;IAClC,MAAM,YAAY,GAAG,qBAAqB,UAAU,QAAQ;IAC5D,eAAe,SAAS,CAAC,CACtB,QAAQ,MAAM,CAAC,EAAE,WAAW,CAAC,CAC7B,SAAS,MAAM;KACd,CAAA,GAAA,WAAA,iBAAA,CAAiB,EAAE,CAAC,CAAC,qBAAqB,UAAU,UAAU,EAAE,WAAW,CAAC,CAAC;KAC7E,EAAE,QAAQ;IACZ,CAAC;GACL,CAAC;EACH,CAAC;CACH;AACF;AAQA,SAAS,iBAAiB,IAAmB,WAAmB,QAAgB,OAA2B;CAKzG,MAAM,YAAa,OAAe;CAClC,UAAmB,OAAO;CAC1B,MAAM,EAAE,UAAA,GAASO,WAAAA,gBAAAA,CAAgB,MAAM;CACvC,KAAK,aAAa,OAAO;CACzB,IAAI,CAAC,OAAO,iBAAiB,UAAU,QAAQ;AACjD;AAEA,SAAS,aAAa,WAAmB,QAAgB,OAAe;CACtE,MAAM,QAAA,GAAOC,WAAAA,YAAAA,CAAY,MAAM;CAC/B,MAAM,wBAAQ,IAAI,IAAI,CAAC,MAAM,GAAG,KAAK,SAAS,CAAC;CAC/C,QAAA,GAAOK,WAAAA,aAAAA,CAAa,KAAK,CAAC,CACvB,OAAO,CAAC,CACR,QAAQ,MAAM,aAAaC,WAAAA,yBAAyB,aAAaH,WAAAA,mBAAmB,CAAC,CACrF,MAAM,MAAM,MAAM,IAAI,EAAE,SAAS,KAAK,EAAE,mBAAmB,SAAS;AACzE;AAEA,SAAS,sBACP,IACA,WACA,QACA,OACA,SAAkB,OACZ;CACN,MAAM,QAAA,GAAOH,WAAAA,YAAAA,CAAY,MAAM;CAC/B,MAAM,YAAY,aAAa,WAAW,QAAQ,KAAK;CAEvD,IAAI,CAAC,WAAW;CAGhB,IAAI,UAAU,aAAa;CAC3B,IAAI;CACJ,IAAI,qBAAqBM,WAAAA,uBACvB,gBAAgB,SAAS,CAAC,IAAI,CAAC,MAAM;MAChC,IAAI,qBAAqBH,WAAAA,qBAE9B,gBAAgB,GACb,YAAY,KAAK,IAAI,CAAC,CACtB,OAAO,CAAC,CACR,QAAQ,MAAM,CAAC,EAAE,eAAe,CAAC,CACjC,QAAQ,MAAQ,EAAU,UAAU,eAAe,CAAuB,kBAAkB,MAAM,QAAQ,CAAC,CAC3G,QAAQ;MAEX,CAAA,GAAA,WAAA,YAAA,CAAY,SAAS;CAIvB,CAAA,GAAA,WAAA,iBAAA,CAAiB,EAAE,CAAC,CAAC,qBAAqB,MAAM,UAAU,UAAU,WAAW,aAAa;CAC5F,UAAU,QAAQ;CAIlB,IAAI,qBAAqBA,WAAAA,qBAAqB,UAAU,cAAc;AACxE;AAEA,SAAS,gBACP,IACA,WACA,MACA,UACA,UACA;CACA,MAAM,OAAA,GAAMX,WAAAA,iBAAAA,CAAiB,EAAE;CAC/B,KAAK,SAAS,QAAQ;EAGpB,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,MAAM,OAAO,OAAO,QAAQ,IAAI,OAAO;EAC3D,CAII;GAAC;GAAM;GAAI;EAAE,GACb;GAAC;GAAM;GAAI;EAAE,CACf,CAAC,CACD,SAAS,CAAC,QAAQ,WAAW,cAAc;GAC3C,MAAM,EAAE,UAAU,OAAO;GACzB,MAAM,YAAY,GAAG,qBAAqB,EAAE;GAC5C,IAAI,UAAU,iBAAiB;GAE/B,MAAM,EAAE,cADO,eAAe,SAAS,CAAC,CAAC,MAAM,MAAM,EAAE,kBAAkB,aAAa,EAAE,eAAe,MAC5E;GAC3B,MAAM,SAAU,UAAkB;GAClC,IAAI;GACJ,IAAI,SAAS,IAAI,MAAM,GAGrB,WAAW,IAAI,qBAAqB,IAAI,SAAS;QAC5C,IAAI,OAAO,UAKhB,WAAW,OAAO;QACb,IAAI,UAAU,aAGnB,WAAW,CAAC;QAEZ,KAAK,YAAY,UAAU,cAAc;GAE3C,SAAS,GAAG,qBAAqB,SAAS,QAAQ,GAAI,QAAQ;GAC9D,IAAI,qBAAqB,IAAI,WAAW,QAAQ;GAGhD,SAAS,IAAI,MAAM;EACrB,CAAC;CACH,CAAC;AACH;AAEA,SAAS,cAAc,QAAmD;CACxE,QAAA,GAAOa,WAAAA,aAAAA,CAAa,MAAM,CAAC,CACxB,OAAO,CAAC,CACR,QACE,MACC,aAAaE,WAAAA,0BACb,aAAaC,WAAAA,yBACb,aAAaC,WAAAA,wBACjB;AACJ;AAEA,SAAS,eACP,QAKA;CACA,QAAA,GAAOJ,WAAAA,aAAAA,CAAa,MAAM,CAAC,CACxB,OAAO,CAAC,CACR,QACE,MACC,aAAaK,WAAAA,wBACb,aAAaC,WAAAA,0BACb,aAAaC,WAAAA,+BACjB;AACJ;AAEA,SAAS,eAAe,QAAgB;CACtC,QAAA,GAAOP,WAAAA,aAAAA,CAAa,MAAM,CAAC,CACxB,OAAO,CAAC,CACR,QACE,MACC,aAAaK,WAAAA,wBACb,aAAaP,WAAAA,uBACb,aAAaG,WAAAA,yBACb,aAAaK,WAAAA,0BACb,aAAaC,WAAAA,mCACb,aAAaC,WAAAA,kBACjB;AACJ"}
1
+ {"version":3,"file":"RunPlugin.cjs","names":["Plugin","getEmInternalApi","#syncEntityData","#syncReferencesAndLazyFields","#syncManyToManys","#syncEnumCollections","#preloadUnloadedRelations","createRowFromEntityData","getInstanceData","getMetadata","PolymorphicKeySerde","PojoRowData","OneToManyCollection","isEntity","getRelations","OneToOneReferenceImpl","ManyToOneReferenceImpl","ReactiveReferenceImpl","PolymorphicReferenceImpl","ManyToManyCollection","ReactiveManyToManyImpl","ReactiveManyToManyOtherSideImpl","EnumCollectionImpl"],"sources":["../src/RunPlugin.ts"],"sourcesContent":["import {\n type Entity,\n type EntityManager,\n EnumCollectionImpl,\n type FieldColumn,\n type JoinRow,\n type JoinRowTodo,\n type LazyFieldImpl,\n ManyToManyCollection,\n ManyToOneReferenceImpl,\n type MaybeAbstractEntityConstructor,\n OneToManyCollection,\n OneToOneReferenceImpl,\n Plugin,\n PojoRowData,\n PolymorphicKeySerde,\n PolymorphicReferenceImpl,\n type PrimitiveField,\n ReactiveManyToManyImpl,\n ReactiveManyToManyOtherSideImpl,\n ReactiveReferenceImpl,\n type Todo,\n assertNever,\n createRowFromEntityData,\n getEmInternalApi,\n getInstanceData,\n getMetadata,\n getRelations,\n isEntity,\n} from \"joist-core\";\n\n/*\n * `run...` helpers use this plugin to mirror changes from their isolated entity manager to the original\n * entity manager they were passed. This allows changes to be synchronously reflected between the two entity managers\n * without needing to query the database.\n */\nexport class RunPlugin extends Plugin {\n // The em passed here is the target which we'll mirror changes to. The plugin should not be added to it, but rather\n // to the new em `run..` helpers create or by downstream ems created by implementation code\n constructor(public readonly em: EntityManager) {\n super();\n }\n\n maybeCloneForNewEm() {\n return this;\n }\n\n afterWrite(entityTodos: Record<string, Todo>, joinRowTodos: Record<string, JoinRowTodo>) {\n const api = getEmInternalApi(this.em);\n // We don't want any old preloaded data leaking into our changes, so we clear it out before we start as a precaution\n api.clearPreloadedRelations();\n // Likewise, any data loaders in memory will have stale data, so we clear them out as well\n api.clearDataloaders();\n this.#syncEntityData(entityTodos);\n this.#syncReferencesAndLazyFields(entityTodos);\n this.#syncManyToManys(joinRowTodos);\n this.#syncEnumCollections(joinRowTodos);\n this.#preloadUnloadedRelations(entityTodos);\n }\n\n #syncEntityData(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const row = createRowFromEntityData(newEntity, { preferOriginalData: false });\n em.hydrate(newEntity.constructor as MaybeAbstractEntityConstructor<Entity>, [row]);\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n // This fail shouldn't really be possible, since any entity should either have been created:\n // * via factory or em.create in the original em\n // * by the new em inside the `run` call, in which case it'd be an insert instead of an update\n // * by a previous afterWrite call, which would have already been an insert and added to the original em\n // * by a previous `run` call, which would have already been an insert as well\n if (!oldEntity) fail(`Expected to find existing entity for $ {e.idTagged}`);\n const { data: newData } = getInstanceData(newEntity);\n const { data: oldData } = getInstanceData(oldEntity);\n const meta = getMetadata(newEntity);\n (newEntity as any).changes.fields\n .values()\n .filter((fieldName: string) => fieldName in newData)\n .filter((fieldName: string) => !!meta.allFields[fieldName].serde)\n .forEach((fieldName: string) => {\n // We're imitating a round trip to the database here, so we use our field's serde to map the value back\n // and forth.\n const serde = meta.allFields[fieldName].serde!;\n let column: FieldColumn | undefined;\n let value: any;\n if (serde instanceof PolymorphicKeySerde) {\n [column, value] =\n serde.columns\n .values()\n .map((c) => [c, c.rowValue(newData)] as const)\n .find(([, v]) => v !== undefined) ?? [];\n } else {\n [column] = serde.columns;\n // the 2nd and 3rd arguments are only used if the 4th argument is defined, so it's OK to pass undefined here\n value = column.rowValue(newData);\n }\n oldData[fieldName] = serde.fromRow(new PojoRowData([column ? { [column.columnName]: value } : {}]), 0);\n });\n });\n todo.deletes.forEach((newEntity) => {\n const existing = em.findExistingInstance(newEntity.idTagged);\n // Again it shouldn't really be possible for a deletion to happen without the entity being present in the\n // original em, but just in case we'll skip if we don't find it\n if (!existing) fail(`Expected to find existing entity for $ {e.idTagged}`);\n getInstanceData(existing).markDeletedBecauseNotFound();\n });\n });\n }\n\n // Ensure m2o/rr/poly changes and known lazy values from the new em are loaded in the original em\n // Run is a test helper, so we eagerly preload the test entity graph for synchronous assertions. LazyFields are an\n // exception: they can hold large payloads, so we only preload values known from inserts or changed columns. Untouched\n // lazy fields keep their loaded state instead of fetching data that the production code did not write.\n #syncReferencesAndLazyFields(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n // New entities always have the lazy field's initial value, so always preload them\n for (const field of getLazyFields(newEntity)) {\n preloadLazyField(oldEntity, field.fieldName);\n }\n\n getReferences(newEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n });\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n // Only preload lazy fields that were actively changed, b/c they're already available/loaded in the entity we're copying from\n for (const field of getLazyFields(newEntity)) {\n if ((newEntity as any).changes[field.fieldName].hasChanged) {\n preloadLazyField(oldEntity, field.fieldName);\n }\n }\n\n getReferences(newEntity)\n .filter((r) => (newEntity as any).changes[r.fieldName].hasChanged)\n .forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n const originalId = (newEntity as any).changes[r.fieldName].originalValue;\n const originalOther = originalId ? em.findExistingInstance(originalId) : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n // If the entity was soft-deleted, then we need to clear out the caches for any o2m collections that contain it\n if (\"deletedAt\" in newEntity && (newEntity as any).changes.deletedAt.hasChanged) {\n getReferences(oldEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n if (other) {\n const otherSide = getOtherSide(r.fieldName, oldEntity, other);\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n }\n });\n }\n });\n todo.deletes.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n // Since both old and new entities are marked as deleted, we can't just go looking through their relations\n // for the other side's id since this will error. Likewise, we only care about the collection that this\n // entity used to belong to in the old em so we can remove it. So just grab the other id directly from the\n // old entity's data and then preload its other side relation.\n const entityOrId = getInstanceData(oldEntity).data[r.fieldName];\n const originalOther = entityOrId\n ? em.findExistingInstance(isEntity(entityOrId) ? entityOrId.idTagged : entityOrId)\n : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n });\n });\n }\n\n // Ensure m2m changes are propagated from the new em to the original em\n #syncManyToManys(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n Object.entries(joinRowTodos).forEach(([joinTable, todo]) => {\n // Enum m2ms (EnumCollections) aren't entity-to-entity, so there's nothing to mirror between ems.\n if (todo.m2m.otherEnum) return;\n const preloads = new Set<ManyToManyCollection<Entity, Entity>>();\n processJoinRows(em, joinTable, todo.newRows, preloads, (oldEntity, entities) => entities.push(oldEntity));\n processJoinRows(em, joinTable, todo.deletedRows, preloads, (oldEntity, entities) =>\n entities.splice(entities.indexOf(oldEntity), 1),\n );\n // Since multiple join rows could reference the same m2m, our processJoinRows doesn't actually call preload() so\n // we need to do it now\n preloads.values().forEach((r) => {\n const id = r.entity.idTagged;\n const entities = api.getPreloadedRelation(id, r.fieldName) as Entity[];\n // unique our entities in case somehow an entity was inserted by multiple join rows\n api.setPreloadedRelation(id, r.fieldName, [...new Set(entities)]);\n r.preload();\n });\n });\n }\n\n // Ensure enum m2m (EnumCollection) membership changes are propagated from the new em to the original em\n #syncEnumCollections(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n const touched = new Set<EnumCollectionImpl<Entity, any>>();\n Object.values(joinRowTodos).forEach((todo) => {\n // Only enum m2ms; entity-to-entity m2ms are handled by `#syncManyToManys`.\n if (!todo.m2m.otherEnum) return;\n const { columnName, otherColumnName, fieldName } = todo.m2m;\n const sync = (rows: JoinRow[], add: boolean) => {\n rows.forEach((row) => {\n const newEntity = row.columns[columnName] as Entity;\n const enumId = row.columns[otherColumnName] as number;\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n if (!oldEntity || oldEntity.isDeletedEntity) return;\n const oldCollection = (oldEntity as any)[fieldName] as EnumCollectionImpl<Entity, any>;\n // Only mirror collections the original em has loaded (or new entities, whose rows are all\n // adds); otherwise a later `.load()` will read the (same-txn) db and see the change itself.\n if (!oldCollection.isLoaded && !newEntity.isNewEntity) return;\n const joinRows = api.joinRows(oldCollection);\n if (add) {\n joinRows.addPreloadedRow(oldCollection, undefined, oldEntity, enumId);\n } else {\n joinRows.removePreloadedRow(oldCollection, oldEntity, enumId);\n }\n touched.add(oldCollection);\n });\n };\n sync(todo.newRows, true);\n sync(todo.deletedRows, false);\n });\n // Re-publish each touched collection's codes and mark it loaded.\n touched.forEach((oldCollection) => {\n const joinRows = api.joinRows(oldCollection);\n const codes = (joinRows.getOthers(oldCollection.columnName, oldCollection.entity) as number[]).map(\n (id) => oldCollection.otherEnum!.findById(id)!.code,\n );\n api.setPreloadedRelation(oldCollection.entity.idTagged, oldCollection.fieldName, codes);\n oldCollection.preload();\n });\n }\n\n // Make sure any unloaded relations on net new entities are marked as loaded. We should have already preloaded all\n // concrete references in a previous step. So we just need to load collections (plus o2os) if they haven't already\n // been preloaded by another step. Since nothing else preloaded them, they must have no data so we can just set\n // them to an empty array.\n #preloadUnloadedRelations(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.entries(entityTodos).forEach(([, todo]) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getCollections(oldEntity)\n .filter((r) => !r.isPreloaded)\n .forEach((r) => {\n getEmInternalApi(em).setPreloadedRelation(oldEntity.idTagged, r.fieldName, []);\n r.preload();\n });\n });\n });\n }\n}\n\n// References that represent one (or more) foreign keys directly on the entity\ntype ConcreteReference =\n | ManyToOneReferenceImpl<Entity, Entity, any>\n | ReactiveReferenceImpl<Entity, Entity, any, any>\n | PolymorphicReferenceImpl<Entity, Entity, any>;\n\n/** Preloads a mirrored lazy value without treating the database write as a test-entity mutation. */\nfunction preloadLazyField(entity: Entity, fieldName: string): void {\n const field = (entity as any)[fieldName] as LazyFieldImpl<Entity, unknown>;\n field.preload();\n}\n\nfunction preloadReference(em: EntityManager, fieldName: string, entity: Entity, other: Entity | undefined) {\n // Preload for concrete references doesn't make us of the global preloaded cache. It just looks at its owner's data\n // to get a key, then checks if that entity is already in the em and uses that if so. So we just need to set the\n // owner's data to have the correct key then we can call preload since all entities should already be in the original\n // em\n const reference = (entity as any)[fieldName] as ConcreteReference;\n (reference as any).unload();\n const { data } = getInstanceData(entity);\n data[fieldName] = other?.idTagged;\n if (!entity.isDeletedEntity) reference.preload();\n}\n\nfunction getOtherSide(fieldName: string, entity: Entity, other: Entity) {\n const meta = getMetadata(entity);\n const metas = new Set([meta, ...meta.baseTypes]);\n return getRelations(other)\n .values()\n .filter((r) => r instanceof OneToOneReferenceImpl || r instanceof OneToManyCollection)\n .find((r) => metas.has(r.otherMeta) && r.otherFieldName === fieldName);\n}\n\nfunction maybePreloadOtherSide(\n em: EntityManager,\n fieldName: string,\n entity: Entity,\n other: Entity,\n remove: boolean = false,\n): void {\n const meta = getMetadata(entity);\n const otherSide = getOtherSide(fieldName, entity, other);\n // If our other side is a large o2m then we can just ignore it, since it isn't loadable regardless\n if (!otherSide) return;\n // Since our preload scans the entire em, we only need to run it once per collection/o2o. We clear out preloads\n // when we start the afterWrite, so if anything is preloaded we know we put it there.\n if (otherSide.isPreloaded) return;\n let otherEntities: Entity[];\n if (otherSide instanceof OneToOneReferenceImpl) {\n otherEntities = remove ? [] : [entity];\n } else if (otherSide instanceof OneToManyCollection) {\n // Scan the entire em for (non-deleted) entities of our type that reference the other side\n otherEntities = em\n .getEntities(meta.cstr)\n .values()\n .filter((e) => !e.isDeletedEntity)\n .filter((e) => ((e as any)[otherSide.otherFieldName] as ConcreteReference).idTaggedMaybe === other.idTagged)\n .toArray();\n } else {\n assertNever(otherSide);\n }\n // We don't need to worry about order here because o2ms do their own sorting internally and o2os are inherently\n // unordered (since it's just a single entity)\n getEmInternalApi(em).setPreloadedRelation(other.idTagged, otherSide.fieldName, otherEntities);\n otherSide.preload();\n // This method is confusingly name, what it actually does is clear out the sorted caches that m2m uses to avoid\n // sorting on every `.get` call. Since we just preloaded the collection with new data, we know these caches need to\n // be cleared\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n}\n\nfunction processJoinRows(\n em: EntityManager,\n joinTable: string,\n rows: JoinRow[],\n preloads: Set<ManyToManyCollection<Entity, Entity>>,\n callback: (oldEntity: Entity, entities: Entity[]) => void,\n) {\n const api = getEmInternalApi(em);\n rows.forEach((row) => {\n // A join row's `columns` is always an object with two key/value pairs, one for each side of the m2m.\n // This only runs for entity-to-entity m2ms (enum m2ms are skipped by the caller), so both are entities.\n const [[col1, e1], [col2, e2]] = Object.entries(row.columns) as [[string, Entity], [string, Entity]];\n (\n [\n // Each join row needs to append/remove from both sides of the m2m. So we need to do the same operation once for\n // each side.\n [col1, e1, e2],\n [col2, e2, e1],\n ] as const\n ).forEach(([column, newEntity, newOther]) => {\n const { idTagged: id } = newEntity;\n const oldEntity = em.findExistingInstance(id)!;\n if (oldEntity.isDeletedEntity) return;\n const newM2m = getManyToManys(newEntity).find((r) => r.joinTableName === joinTable && r.columnName === column)!;\n const { fieldName } = newM2m;\n const oldM2m = (oldEntity as any)[fieldName] as ManyToManyCollection<Entity, Entity>;\n let entities: Entity[];\n if (preloads.has(oldM2m)) {\n // If a previous join row has already preloaded this relation, then we can just append/remove from the\n // existing preloaded data.\n entities = api.getPreloadedRelation(id, fieldName) as Entity[];\n } else if (oldM2m.isLoaded) {\n // The order here is important. M2ms are returned in the order their join rows were inserted, so we need to\n // make sure we match that behavior here. So we get all the extant rows first and then append each new row\n // in order from there. Removals are fine since they'll modify the array in place and the correct order will\n // be preserved.\n entities = oldM2m.get;\n } else if (newEntity.isNewEntity) {\n // If this is a new entity, then all the join rows referencing it must also be inserts. So we can just use an\n // empty array initially and inserts will be appended to it.\n entities = [];\n } else {\n fail(`Expected ${fieldName} to be loaded`);\n }\n callback(em.findExistingInstance(newOther.idTagged)!, entities);\n api.setPreloadedRelation(id, fieldName, entities);\n // We only need to preload the m2m once, but multiple join rows could reference it, so we keep track of which m2ms\n // need to be preloaded as we work, then actually call preload() at the end once we're done.\n preloads.add(oldM2m);\n });\n });\n}\n\n/** Returns the lazy columns on an entity, including inherited fields. */\nfunction getLazyFields(entity: Entity): IteratorObject<PrimitiveField> {\n return Object.values(getMetadata(entity).allFields)\n .values()\n .filter((field) => field.kind === \"primitive\")\n .filter((field) => !!field.lazy);\n}\n\nfunction getReferences(entity: Entity): IteratorObject<ConcreteReference> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToOneReferenceImpl ||\n r instanceof ReactiveReferenceImpl ||\n r instanceof PolymorphicReferenceImpl,\n );\n}\n\nfunction getManyToManys(\n entity: Entity,\n): IteratorObject<\n | ManyToManyCollection<Entity, Entity>\n | ReactiveManyToManyImpl<any, any, any>\n | ReactiveManyToManyOtherSideImpl<any, any>\n> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl,\n );\n}\n\nfunction getCollections(entity: Entity) {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof OneToManyCollection ||\n r instanceof OneToOneReferenceImpl ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl ||\n r instanceof EnumCollectionImpl,\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunPlugin.d.cts","names":[],"sources":["../src/RunPlugin.ts"],"mappings":";;cAkCa,kBAAkB;;WAGD,IAAI;EAAhC,YAA4B,IAAI;EAIhC;EAIA,WAAW,aAAa,eAAe,OAAO,cAAc,eAAe"}
1
+ {"version":3,"file":"RunPlugin.d.cts","names":[],"sources":["../src/RunPlugin.ts"],"mappings":";;cAoCa,kBAAkB;;WAGD,IAAI;EAAhC,YAA4B,IAAI;EAIhC;EAIA,WAAW,aAAa,eAAe,OAAO,cAAc,eAAe"}
@@ -1 +1 @@
1
- {"version":3,"file":"RunPlugin.d.mts","names":[],"sources":["../src/RunPlugin.ts"],"mappings":";;cAkCa,kBAAkB;;WAGD,IAAI;EAAhC,YAA4B,IAAI;EAIhC;EAIA,WAAW,aAAa,eAAe,OAAO,cAAc,eAAe"}
1
+ {"version":3,"file":"RunPlugin.d.mts","names":[],"sources":["../src/RunPlugin.ts"],"mappings":";;cAoCa,kBAAkB;;WAGD,IAAI;EAAhC,YAA4B,IAAI;EAIhC;EAIA,WAAW,aAAa,eAAe,OAAO,cAAc,eAAe"}
@@ -14,7 +14,7 @@ var RunPlugin = class extends Plugin {
14
14
  api.clearPreloadedRelations();
15
15
  api.clearDataloaders();
16
16
  this.#syncEntityData(entityTodos);
17
- this.#syncReferences(entityTodos);
17
+ this.#syncReferencesAndLazyFields(entityTodos);
18
18
  this.#syncManyToManys(joinRowTodos);
19
19
  this.#syncEnumCollections(joinRowTodos);
20
20
  this.#preloadUnloadedRelations(entityTodos);
@@ -51,11 +51,12 @@ var RunPlugin = class extends Plugin {
51
51
  });
52
52
  });
53
53
  }
54
- #syncReferences(entityTodos) {
54
+ #syncReferencesAndLazyFields(entityTodos) {
55
55
  const { em } = this;
56
56
  Object.values(entityTodos).forEach((todo) => {
57
57
  todo.inserts.forEach((newEntity) => {
58
58
  const oldEntity = em.findExistingInstance(newEntity.idTagged);
59
+ for (const field of getLazyFields(newEntity)) preloadLazyField(oldEntity, field.fieldName);
59
60
  getReferences(newEntity).forEach((r) => {
60
61
  const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : void 0;
61
62
  preloadReference(em, r.fieldName, oldEntity, other);
@@ -64,6 +65,7 @@ var RunPlugin = class extends Plugin {
64
65
  });
65
66
  todo.updates.forEach((newEntity) => {
66
67
  const oldEntity = em.findExistingInstance(newEntity.idTagged);
68
+ for (const field of getLazyFields(newEntity)) if (newEntity.changes[field.fieldName].hasChanged) preloadLazyField(oldEntity, field.fieldName);
67
69
  getReferences(newEntity).filter((r) => newEntity.changes[r.fieldName].hasChanged).forEach((r) => {
68
70
  const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : void 0;
69
71
  preloadReference(em, r.fieldName, oldEntity, other);
@@ -149,6 +151,10 @@ var RunPlugin = class extends Plugin {
149
151
  });
150
152
  }
151
153
  };
154
+ /** Preloads a mirrored lazy value without treating the database write as a test-entity mutation. */
155
+ function preloadLazyField(entity, fieldName) {
156
+ entity[fieldName].preload();
157
+ }
152
158
  function preloadReference(em, fieldName, entity, other) {
153
159
  const reference = entity[fieldName];
154
160
  reference.unload();
@@ -203,6 +209,10 @@ function processJoinRows(em, joinTable, rows, preloads, callback) {
203
209
  });
204
210
  });
205
211
  }
212
+ /** Returns the lazy columns on an entity, including inherited fields. */
213
+ function getLazyFields(entity) {
214
+ return Object.values(getMetadata(entity).allFields).values().filter((field) => field.kind === "primitive").filter((field) => !!field.lazy);
215
+ }
206
216
  function getReferences(entity) {
207
217
  return getRelations(entity).values().filter((r) => r instanceof ManyToOneReferenceImpl || r instanceof ReactiveReferenceImpl || r instanceof PolymorphicReferenceImpl);
208
218
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RunPlugin.js","names":["#syncEntityData","#syncReferences","#syncManyToManys","#syncEnumCollections","#preloadUnloadedRelations"],"sources":["../src/RunPlugin.ts"],"sourcesContent":["import {\n type Entity,\n type EntityManager,\n EnumCollectionImpl,\n type FieldColumn,\n type JoinRow,\n type JoinRowTodo,\n ManyToManyCollection,\n ManyToOneReferenceImpl,\n type MaybeAbstractEntityConstructor,\n OneToManyCollection,\n OneToOneReferenceImpl,\n Plugin,\n PojoRowData,\n PolymorphicKeySerde,\n PolymorphicReferenceImpl,\n ReactiveManyToManyImpl,\n ReactiveManyToManyOtherSideImpl,\n ReactiveReferenceImpl,\n type Todo,\n assertNever,\n createRowFromEntityData,\n getEmInternalApi,\n getInstanceData,\n getMetadata,\n getRelations,\n isEntity,\n} from \"joist-core\";\n\n/*\n * `run...` helpers use this plugin to mirror any changes made to their isolated entity manager inside to the original\n * entity manager they were passed. This allows changes to be synchronously reflected between the two entity managers\n * without needing to query the database.\n */\nexport class RunPlugin extends Plugin {\n // The em passed here is the target which we'll mirror changes to. The plugin should not be added to it, but rather\n // to the new em `run..` helpers create or by downstream ems created by implementation code\n constructor(public readonly em: EntityManager) {\n super();\n }\n\n maybeCloneForNewEm() {\n return this;\n }\n\n afterWrite(entityTodos: Record<string, Todo>, joinRowTodos: Record<string, JoinRowTodo>) {\n const api = getEmInternalApi(this.em);\n // We don't want any old preloaded data leaking into our changes, so we clear it out before we start as a precaution\n api.clearPreloadedRelations();\n // Likewise, any data loaders in memory will have stale data, so we clear them out as well\n api.clearDataloaders();\n this.#syncEntityData(entityTodos);\n this.#syncReferences(entityTodos);\n this.#syncManyToManys(joinRowTodos);\n this.#syncEnumCollections(joinRowTodos);\n this.#preloadUnloadedRelations(entityTodos);\n }\n\n #syncEntityData(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const row = createRowFromEntityData(newEntity, { preferOriginalData: false });\n em.hydrate(newEntity.constructor as MaybeAbstractEntityConstructor<any>, [row]);\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n // This fail shouldn't really be possible, since any entity should either have been created:\n // * via factory or em.create in the original em\n // * by the new em inside the `run` call, in which case it'd be an insert instead of an update\n // * by a previous afterWrite call, which would have already been an insert and added to the original em\n // * by a previous `run` call, which would have already been an insert as well\n if (!oldEntity) fail(`Expected to find existing entity for $ {e.idTagged}`);\n const { data: newData } = getInstanceData(newEntity);\n const { data: oldData } = getInstanceData(oldEntity);\n const meta = getMetadata(newEntity);\n (newEntity as any).changes.fields\n .values()\n .filter((fieldName: string) => fieldName in newData)\n .filter((fieldName: string) => !!meta.allFields[fieldName].serde)\n .forEach((fieldName: string) => {\n // We're imitating a round trip to the database here, so we use our field's serde to map the value back\n // and forth.\n const serde = meta.allFields[fieldName].serde!;\n let column: FieldColumn | undefined;\n let value: any;\n if (serde instanceof PolymorphicKeySerde) {\n [column, value] =\n serde.columns\n .values()\n .map((c) => [c, c.rowValue(newData)] as const)\n .find(([, v]) => v !== undefined) ?? [];\n } else {\n [column] = serde.columns;\n // the 2nd and 3rd arguments are only used if the 4th argument is defined, so it's OK to pass undefined here\n value = column.rowValue(newData);\n }\n oldData[fieldName] = serde.fromRow(new PojoRowData([column ? { [column.columnName]: value } : {}]), 0);\n });\n });\n todo.deletes.forEach((newEntity) => {\n const existing = em.findExistingInstance(newEntity.idTagged);\n // Again it shouldn't really be possible for a deletion to happen without the entity being present in the\n // original em, but just in case we'll skip if we don't find it\n if (!existing) fail(`Expected to find existing entity for $ {e.idTagged}`);\n getInstanceData(existing).markDeletedBecauseNotFound();\n });\n });\n }\n\n // Ensure m2o/rr/poly changes from the new em are reflected in the original em\n #syncReferences(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n });\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity)\n .filter((r) => (newEntity as any).changes[r.fieldName].hasChanged)\n .forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n const originalId = (newEntity as any).changes[r.fieldName].originalValue;\n const originalOther = originalId ? em.findExistingInstance(originalId) : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n // If the entity was soft-deleted, then we need to clear out the caches for any o2m collections that contain it\n if (\"deletedAt\" in newEntity && (newEntity as any).changes.deletedAt.hasChanged) {\n getReferences(oldEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n if (other) {\n const otherSide = getOtherSide(r.fieldName, oldEntity, other);\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n }\n });\n }\n });\n todo.deletes.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n // Since both old and new entities are marked as deleted, we can't just go looking through their relations\n // for the other side's id since this will error. Likewise, we only care about the collection that this\n // entity used to belong to in the old em so we can remove it. So just grab the other id directly from the\n // old entity's data and then preload its other side relation.\n const entityOrId = getInstanceData(oldEntity).data[r.fieldName];\n const originalOther = entityOrId\n ? em.findExistingInstance(isEntity(entityOrId) ? entityOrId.idTagged : entityOrId)\n : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n });\n });\n }\n\n // Ensure m2m changes are propagated from the new em to the original em\n #syncManyToManys(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n Object.entries(joinRowTodos).forEach(([joinTable, todo]) => {\n // Enum m2ms (EnumCollections) aren't entity-to-entity, so there's nothing to mirror between ems.\n if (todo.m2m.otherEnum) return;\n const preloads = new Set<ManyToManyCollection<Entity, Entity>>();\n processJoinRows(em, joinTable, todo.newRows, preloads, (oldEntity, entities) => entities.push(oldEntity));\n processJoinRows(em, joinTable, todo.deletedRows, preloads, (oldEntity, entities) =>\n entities.splice(entities.indexOf(oldEntity), 1),\n );\n // Since multiple join rows could reference the same m2m, our processJoinRows doesn't actually call preload() so\n // we need to do it now\n preloads.values().forEach((r) => {\n const id = r.entity.idTagged;\n const entities = api.getPreloadedRelation(id, r.fieldName) as Entity[];\n // unique our entities in case somehow an entity was inserted by multiple join rows\n api.setPreloadedRelation(id, r.fieldName, [...new Set(entities)]);\n r.preload();\n });\n });\n }\n\n // Ensure enum m2m (EnumCollection) membership changes are propagated from the new em to the original em\n #syncEnumCollections(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n const touched = new Set<EnumCollectionImpl<Entity, any>>();\n Object.values(joinRowTodos).forEach((todo) => {\n // Only enum m2ms; entity-to-entity m2ms are handled by `#syncManyToManys`.\n if (!todo.m2m.otherEnum) return;\n const { columnName, otherColumnName, fieldName } = todo.m2m;\n const sync = (rows: JoinRow[], add: boolean) => {\n rows.forEach((row) => {\n const newEntity = row.columns[columnName] as Entity;\n const enumId = row.columns[otherColumnName] as number;\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n if (!oldEntity || oldEntity.isDeletedEntity) return;\n const oldCollection = (oldEntity as any)[fieldName] as EnumCollectionImpl<Entity, any>;\n // Only mirror collections the original em has loaded (or new entities, whose rows are all\n // adds); otherwise a later `.load()` will read the (same-txn) db and see the change itself.\n if (!oldCollection.isLoaded && !newEntity.isNewEntity) return;\n const joinRows = api.joinRows(oldCollection);\n if (add) {\n joinRows.addPreloadedRow(oldCollection, undefined, oldEntity, enumId);\n } else {\n joinRows.removePreloadedRow(oldCollection, oldEntity, enumId);\n }\n touched.add(oldCollection);\n });\n };\n sync(todo.newRows, true);\n sync(todo.deletedRows, false);\n });\n // Re-publish each touched collection's codes and mark it loaded.\n touched.forEach((oldCollection) => {\n const joinRows = api.joinRows(oldCollection);\n const codes = (joinRows.getOthers(oldCollection.columnName, oldCollection.entity) as number[]).map(\n (id) => oldCollection.otherEnum!.findById(id)!.code,\n );\n api.setPreloadedRelation(oldCollection.entity.idTagged, oldCollection.fieldName, codes);\n oldCollection.preload();\n });\n }\n\n // Make sure any unloaded relations on net new entities are marked as loaded. We should have already preloaded all\n // concrete references in a previous step. So we just need to load collections (plus o2os) if they haven't already\n // been preloaded by another step. Since nothing else preloaded them, they must have no data so we can just set\n // them to an empty array.\n #preloadUnloadedRelations(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.entries(entityTodos).forEach(([, todo]) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getCollections(oldEntity)\n .filter((r) => !r.isPreloaded)\n .forEach((r) => {\n getEmInternalApi(em).setPreloadedRelation(oldEntity.idTagged, r.fieldName, []);\n r.preload();\n });\n });\n });\n }\n}\n\n// References that represent one (or more) foreign keys directly on the entity\ntype ConcreteReference =\n | ManyToOneReferenceImpl<Entity, Entity, any>\n | ReactiveReferenceImpl<Entity, Entity, any, any>\n | PolymorphicReferenceImpl<Entity, Entity, any>;\n\nfunction preloadReference(em: EntityManager, fieldName: string, entity: Entity, other: Entity | undefined) {\n // Preload for concrete references doesn't make us of the global preloaded cache. It just looks at its owner's data\n // to get a key, then checks if that entity is already in the em and uses that if so. So we just need to set the\n // owner's data to have the correct key then we can call preload since all entities should already be in the original\n // em\n const reference = (entity as any)[fieldName] as ConcreteReference;\n (reference as any).unload();\n const { data } = getInstanceData(entity);\n data[fieldName] = other?.idTagged;\n if (!entity.isDeletedEntity) reference.preload();\n}\n\nfunction getOtherSide(fieldName: string, entity: Entity, other: Entity) {\n const meta = getMetadata(entity);\n const metas = new Set([meta, ...meta.baseTypes]);\n return getRelations(other)\n .values()\n .filter((r) => r instanceof OneToOneReferenceImpl || r instanceof OneToManyCollection)\n .find((r) => metas.has(r.otherMeta) && r.otherFieldName === fieldName);\n}\n\nfunction maybePreloadOtherSide(\n em: EntityManager,\n fieldName: string,\n entity: Entity,\n other: Entity,\n remove: boolean = false,\n): void {\n const meta = getMetadata(entity);\n const otherSide = getOtherSide(fieldName, entity, other);\n // If our other side is a large o2m then we can just ignore it, since it isn't loadable regardless\n if (!otherSide) return;\n // Since our preload scans the entire em, we only need to run it once per collection/o2o. We clear out preloads\n // when we start the afterWrite, so if anything is preloaded we know we put it there.\n if (otherSide.isPreloaded) return;\n let otherEntities: Entity[];\n if (otherSide instanceof OneToOneReferenceImpl) {\n otherEntities = remove ? [] : [entity];\n } else if (otherSide instanceof OneToManyCollection) {\n // Scan the entire em for (non-deleted) entities of our type that reference the other side\n otherEntities = em\n .getEntities(meta.cstr)\n .values()\n .filter((e) => !e.isDeletedEntity)\n .filter((e) => ((e as any)[otherSide.otherFieldName] as ConcreteReference).idTaggedMaybe === other.idTagged)\n .toArray();\n } else {\n assertNever(otherSide);\n }\n // We don't need to worry about order here because o2ms do their own sorting internally and o2os are inherently\n // unordered (since it's just a single entity)\n getEmInternalApi(em).setPreloadedRelation(other.idTagged, otherSide.fieldName, otherEntities);\n otherSide.preload();\n // This method is confusingly name, what it actually does is clear out the sorted caches that m2m uses to avoid\n // sorting on every `.get` call. Since we just preloaded the collection with new data, we know these caches need to\n // be cleared\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n}\n\nfunction processJoinRows(\n em: EntityManager,\n joinTable: string,\n rows: JoinRow[],\n preloads: Set<ManyToManyCollection<Entity, Entity>>,\n callback: (oldEntity: Entity, entities: Entity[]) => void,\n) {\n const api = getEmInternalApi(em);\n rows.forEach((row) => {\n // A join row's `columns` is always an object with two key/value pairs, one for each side of the m2m.\n // This only runs for entity-to-entity m2ms (enum m2ms are skipped by the caller), so both are entities.\n const [[col1, e1], [col2, e2]] = Object.entries(row.columns) as [[string, Entity], [string, Entity]];\n (\n [\n // Each join row needs to append/remove from both sides of the m2m. So we need to do the same operation once for\n // each side.\n [col1, e1, e2],\n [col2, e2, e1],\n ] as const\n ).forEach(([column, newEntity, newOther]) => {\n const { idTagged: id } = newEntity;\n const oldEntity = em.findExistingInstance(id)!;\n if (oldEntity.isDeletedEntity) return;\n const newM2m = getManyToManys(newEntity).find((r) => r.joinTableName === joinTable && r.columnName === column)!;\n const { fieldName } = newM2m;\n const oldM2m = (oldEntity as any)[fieldName] as ManyToManyCollection<Entity, Entity>;\n let entities: Entity[];\n if (preloads.has(oldM2m)) {\n // If a previous join row has already preloaded this relation, then we can just append/remove from the\n // existing preloaded data.\n entities = api.getPreloadedRelation(id, fieldName) as Entity[];\n } else if (oldM2m.isLoaded) {\n // The order here is important. M2ms are returned in the order their join rows were inserted, so we need to\n // make sure we match that behavior here. So we get all the extant rows first and then append each new row\n // in order from there. Removals are fine since they'll modify the array in place and the correct order will\n // be preserved.\n entities = oldM2m.get;\n } else if (newEntity.isNewEntity) {\n // If this is a new entity, then all the join rows referencing it must also be inserts. So we can just use an\n // empty array initially and inserts will be appended to it.\n entities = [];\n } else {\n fail(`Expected ${fieldName} to be loaded`);\n }\n callback(em.findExistingInstance(newOther.idTagged)!, entities);\n api.setPreloadedRelation(id, fieldName, entities);\n // We only need to preload the m2m once, but multiple join rows could reference it, so we keep track of which m2ms\n // need to be preloaded as we work, then actually call preload() at the end once we're done.\n preloads.add(oldM2m);\n });\n });\n}\n\nfunction getReferences(entity: Entity): IteratorObject<ConcreteReference> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToOneReferenceImpl ||\n r instanceof ReactiveReferenceImpl ||\n r instanceof PolymorphicReferenceImpl,\n );\n}\n\nfunction getManyToManys(\n entity: Entity,\n): IteratorObject<\n | ManyToManyCollection<Entity, Entity>\n | ReactiveManyToManyImpl<any, any, any>\n | ReactiveManyToManyOtherSideImpl<any, any>\n> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl,\n );\n}\n\nfunction getCollections(entity: Entity) {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof OneToManyCollection ||\n r instanceof OneToOneReferenceImpl ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl ||\n r instanceof EnumCollectionImpl,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RunPlugin.js","names":["#syncEntityData","#syncReferencesAndLazyFields","#syncManyToManys","#syncEnumCollections","#preloadUnloadedRelations"],"sources":["../src/RunPlugin.ts"],"sourcesContent":["import {\n type Entity,\n type EntityManager,\n EnumCollectionImpl,\n type FieldColumn,\n type JoinRow,\n type JoinRowTodo,\n type LazyFieldImpl,\n ManyToManyCollection,\n ManyToOneReferenceImpl,\n type MaybeAbstractEntityConstructor,\n OneToManyCollection,\n OneToOneReferenceImpl,\n Plugin,\n PojoRowData,\n PolymorphicKeySerde,\n PolymorphicReferenceImpl,\n type PrimitiveField,\n ReactiveManyToManyImpl,\n ReactiveManyToManyOtherSideImpl,\n ReactiveReferenceImpl,\n type Todo,\n assertNever,\n createRowFromEntityData,\n getEmInternalApi,\n getInstanceData,\n getMetadata,\n getRelations,\n isEntity,\n} from \"joist-core\";\n\n/*\n * `run...` helpers use this plugin to mirror changes from their isolated entity manager to the original\n * entity manager they were passed. This allows changes to be synchronously reflected between the two entity managers\n * without needing to query the database.\n */\nexport class RunPlugin extends Plugin {\n // The em passed here is the target which we'll mirror changes to. The plugin should not be added to it, but rather\n // to the new em `run..` helpers create or by downstream ems created by implementation code\n constructor(public readonly em: EntityManager) {\n super();\n }\n\n maybeCloneForNewEm() {\n return this;\n }\n\n afterWrite(entityTodos: Record<string, Todo>, joinRowTodos: Record<string, JoinRowTodo>) {\n const api = getEmInternalApi(this.em);\n // We don't want any old preloaded data leaking into our changes, so we clear it out before we start as a precaution\n api.clearPreloadedRelations();\n // Likewise, any data loaders in memory will have stale data, so we clear them out as well\n api.clearDataloaders();\n this.#syncEntityData(entityTodos);\n this.#syncReferencesAndLazyFields(entityTodos);\n this.#syncManyToManys(joinRowTodos);\n this.#syncEnumCollections(joinRowTodos);\n this.#preloadUnloadedRelations(entityTodos);\n }\n\n #syncEntityData(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const row = createRowFromEntityData(newEntity, { preferOriginalData: false });\n em.hydrate(newEntity.constructor as MaybeAbstractEntityConstructor<Entity>, [row]);\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n // This fail shouldn't really be possible, since any entity should either have been created:\n // * via factory or em.create in the original em\n // * by the new em inside the `run` call, in which case it'd be an insert instead of an update\n // * by a previous afterWrite call, which would have already been an insert and added to the original em\n // * by a previous `run` call, which would have already been an insert as well\n if (!oldEntity) fail(`Expected to find existing entity for $ {e.idTagged}`);\n const { data: newData } = getInstanceData(newEntity);\n const { data: oldData } = getInstanceData(oldEntity);\n const meta = getMetadata(newEntity);\n (newEntity as any).changes.fields\n .values()\n .filter((fieldName: string) => fieldName in newData)\n .filter((fieldName: string) => !!meta.allFields[fieldName].serde)\n .forEach((fieldName: string) => {\n // We're imitating a round trip to the database here, so we use our field's serde to map the value back\n // and forth.\n const serde = meta.allFields[fieldName].serde!;\n let column: FieldColumn | undefined;\n let value: any;\n if (serde instanceof PolymorphicKeySerde) {\n [column, value] =\n serde.columns\n .values()\n .map((c) => [c, c.rowValue(newData)] as const)\n .find(([, v]) => v !== undefined) ?? [];\n } else {\n [column] = serde.columns;\n // the 2nd and 3rd arguments are only used if the 4th argument is defined, so it's OK to pass undefined here\n value = column.rowValue(newData);\n }\n oldData[fieldName] = serde.fromRow(new PojoRowData([column ? { [column.columnName]: value } : {}]), 0);\n });\n });\n todo.deletes.forEach((newEntity) => {\n const existing = em.findExistingInstance(newEntity.idTagged);\n // Again it shouldn't really be possible for a deletion to happen without the entity being present in the\n // original em, but just in case we'll skip if we don't find it\n if (!existing) fail(`Expected to find existing entity for $ {e.idTagged}`);\n getInstanceData(existing).markDeletedBecauseNotFound();\n });\n });\n }\n\n // Ensure m2o/rr/poly changes and known lazy values from the new em are loaded in the original em\n // Run is a test helper, so we eagerly preload the test entity graph for synchronous assertions. LazyFields are an\n // exception: they can hold large payloads, so we only preload values known from inserts or changed columns. Untouched\n // lazy fields keep their loaded state instead of fetching data that the production code did not write.\n #syncReferencesAndLazyFields(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.values(entityTodos).forEach((todo) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n // New entities always have the lazy field's initial value, so always preload them\n for (const field of getLazyFields(newEntity)) {\n preloadLazyField(oldEntity, field.fieldName);\n }\n\n getReferences(newEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n });\n });\n todo.updates.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n // Only preload lazy fields that were actively changed, b/c they're already available/loaded in the entity we're copying from\n for (const field of getLazyFields(newEntity)) {\n if ((newEntity as any).changes[field.fieldName].hasChanged) {\n preloadLazyField(oldEntity, field.fieldName);\n }\n }\n\n getReferences(newEntity)\n .filter((r) => (newEntity as any).changes[r.fieldName].hasChanged)\n .forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n preloadReference(em, r.fieldName, oldEntity, other);\n if (other) maybePreloadOtherSide(em, r.fieldName, oldEntity, other);\n const originalId = (newEntity as any).changes[r.fieldName].originalValue;\n const originalOther = originalId ? em.findExistingInstance(originalId) : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n // If the entity was soft-deleted, then we need to clear out the caches for any o2m collections that contain it\n if (\"deletedAt\" in newEntity && (newEntity as any).changes.deletedAt.hasChanged) {\n getReferences(oldEntity).forEach((r) => {\n const other = r.isSet ? em.findExistingInstance(r.idTaggedMaybe) : undefined;\n if (other) {\n const otherSide = getOtherSide(r.fieldName, oldEntity, other);\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n }\n });\n }\n });\n todo.deletes.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getReferences(newEntity).forEach((r) => {\n // Since both old and new entities are marked as deleted, we can't just go looking through their relations\n // for the other side's id since this will error. Likewise, we only care about the collection that this\n // entity used to belong to in the old em so we can remove it. So just grab the other id directly from the\n // old entity's data and then preload its other side relation.\n const entityOrId = getInstanceData(oldEntity).data[r.fieldName];\n const originalOther = entityOrId\n ? em.findExistingInstance(isEntity(entityOrId) ? entityOrId.idTagged : entityOrId)\n : undefined;\n if (originalOther) maybePreloadOtherSide(em, r.fieldName, oldEntity, originalOther, true);\n });\n });\n });\n }\n\n // Ensure m2m changes are propagated from the new em to the original em\n #syncManyToManys(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n Object.entries(joinRowTodos).forEach(([joinTable, todo]) => {\n // Enum m2ms (EnumCollections) aren't entity-to-entity, so there's nothing to mirror between ems.\n if (todo.m2m.otherEnum) return;\n const preloads = new Set<ManyToManyCollection<Entity, Entity>>();\n processJoinRows(em, joinTable, todo.newRows, preloads, (oldEntity, entities) => entities.push(oldEntity));\n processJoinRows(em, joinTable, todo.deletedRows, preloads, (oldEntity, entities) =>\n entities.splice(entities.indexOf(oldEntity), 1),\n );\n // Since multiple join rows could reference the same m2m, our processJoinRows doesn't actually call preload() so\n // we need to do it now\n preloads.values().forEach((r) => {\n const id = r.entity.idTagged;\n const entities = api.getPreloadedRelation(id, r.fieldName) as Entity[];\n // unique our entities in case somehow an entity was inserted by multiple join rows\n api.setPreloadedRelation(id, r.fieldName, [...new Set(entities)]);\n r.preload();\n });\n });\n }\n\n // Ensure enum m2m (EnumCollection) membership changes are propagated from the new em to the original em\n #syncEnumCollections(joinRowTodos: Record<string, JoinRowTodo>) {\n const { em } = this;\n const api = getEmInternalApi(em);\n const touched = new Set<EnumCollectionImpl<Entity, any>>();\n Object.values(joinRowTodos).forEach((todo) => {\n // Only enum m2ms; entity-to-entity m2ms are handled by `#syncManyToManys`.\n if (!todo.m2m.otherEnum) return;\n const { columnName, otherColumnName, fieldName } = todo.m2m;\n const sync = (rows: JoinRow[], add: boolean) => {\n rows.forEach((row) => {\n const newEntity = row.columns[columnName] as Entity;\n const enumId = row.columns[otherColumnName] as number;\n const oldEntity = em.findExistingInstance(newEntity.idTagged);\n if (!oldEntity || oldEntity.isDeletedEntity) return;\n const oldCollection = (oldEntity as any)[fieldName] as EnumCollectionImpl<Entity, any>;\n // Only mirror collections the original em has loaded (or new entities, whose rows are all\n // adds); otherwise a later `.load()` will read the (same-txn) db and see the change itself.\n if (!oldCollection.isLoaded && !newEntity.isNewEntity) return;\n const joinRows = api.joinRows(oldCollection);\n if (add) {\n joinRows.addPreloadedRow(oldCollection, undefined, oldEntity, enumId);\n } else {\n joinRows.removePreloadedRow(oldCollection, oldEntity, enumId);\n }\n touched.add(oldCollection);\n });\n };\n sync(todo.newRows, true);\n sync(todo.deletedRows, false);\n });\n // Re-publish each touched collection's codes and mark it loaded.\n touched.forEach((oldCollection) => {\n const joinRows = api.joinRows(oldCollection);\n const codes = (joinRows.getOthers(oldCollection.columnName, oldCollection.entity) as number[]).map(\n (id) => oldCollection.otherEnum!.findById(id)!.code,\n );\n api.setPreloadedRelation(oldCollection.entity.idTagged, oldCollection.fieldName, codes);\n oldCollection.preload();\n });\n }\n\n // Make sure any unloaded relations on net new entities are marked as loaded. We should have already preloaded all\n // concrete references in a previous step. So we just need to load collections (plus o2os) if they haven't already\n // been preloaded by another step. Since nothing else preloaded them, they must have no data so we can just set\n // them to an empty array.\n #preloadUnloadedRelations(entityTodos: Record<string, Todo>) {\n const { em } = this;\n Object.entries(entityTodos).forEach(([, todo]) => {\n todo.inserts.forEach((newEntity) => {\n const oldEntity = em.findExistingInstance(newEntity.idTagged)!;\n getCollections(oldEntity)\n .filter((r) => !r.isPreloaded)\n .forEach((r) => {\n getEmInternalApi(em).setPreloadedRelation(oldEntity.idTagged, r.fieldName, []);\n r.preload();\n });\n });\n });\n }\n}\n\n// References that represent one (or more) foreign keys directly on the entity\ntype ConcreteReference =\n | ManyToOneReferenceImpl<Entity, Entity, any>\n | ReactiveReferenceImpl<Entity, Entity, any, any>\n | PolymorphicReferenceImpl<Entity, Entity, any>;\n\n/** Preloads a mirrored lazy value without treating the database write as a test-entity mutation. */\nfunction preloadLazyField(entity: Entity, fieldName: string): void {\n const field = (entity as any)[fieldName] as LazyFieldImpl<Entity, unknown>;\n field.preload();\n}\n\nfunction preloadReference(em: EntityManager, fieldName: string, entity: Entity, other: Entity | undefined) {\n // Preload for concrete references doesn't make us of the global preloaded cache. It just looks at its owner's data\n // to get a key, then checks if that entity is already in the em and uses that if so. So we just need to set the\n // owner's data to have the correct key then we can call preload since all entities should already be in the original\n // em\n const reference = (entity as any)[fieldName] as ConcreteReference;\n (reference as any).unload();\n const { data } = getInstanceData(entity);\n data[fieldName] = other?.idTagged;\n if (!entity.isDeletedEntity) reference.preload();\n}\n\nfunction getOtherSide(fieldName: string, entity: Entity, other: Entity) {\n const meta = getMetadata(entity);\n const metas = new Set([meta, ...meta.baseTypes]);\n return getRelations(other)\n .values()\n .filter((r) => r instanceof OneToOneReferenceImpl || r instanceof OneToManyCollection)\n .find((r) => metas.has(r.otherMeta) && r.otherFieldName === fieldName);\n}\n\nfunction maybePreloadOtherSide(\n em: EntityManager,\n fieldName: string,\n entity: Entity,\n other: Entity,\n remove: boolean = false,\n): void {\n const meta = getMetadata(entity);\n const otherSide = getOtherSide(fieldName, entity, other);\n // If our other side is a large o2m then we can just ignore it, since it isn't loadable regardless\n if (!otherSide) return;\n // Since our preload scans the entire em, we only need to run it once per collection/o2o. We clear out preloads\n // when we start the afterWrite, so if anything is preloaded we know we put it there.\n if (otherSide.isPreloaded) return;\n let otherEntities: Entity[];\n if (otherSide instanceof OneToOneReferenceImpl) {\n otherEntities = remove ? [] : [entity];\n } else if (otherSide instanceof OneToManyCollection) {\n // Scan the entire em for (non-deleted) entities of our type that reference the other side\n otherEntities = em\n .getEntities(meta.cstr)\n .values()\n .filter((e) => !e.isDeletedEntity)\n .filter((e) => ((e as any)[otherSide.otherFieldName] as ConcreteReference).idTaggedMaybe === other.idTagged)\n .toArray();\n } else {\n assertNever(otherSide);\n }\n // We don't need to worry about order here because o2ms do their own sorting internally and o2os are inherently\n // unordered (since it's just a single entity)\n getEmInternalApi(em).setPreloadedRelation(other.idTagged, otherSide.fieldName, otherEntities);\n otherSide.preload();\n // This method is confusingly name, what it actually does is clear out the sorted caches that m2m uses to avoid\n // sorting on every `.get` call. Since we just preloaded the collection with new data, we know these caches need to\n // be cleared\n if (otherSide instanceof OneToManyCollection) otherSide.resetIsLoaded();\n}\n\nfunction processJoinRows(\n em: EntityManager,\n joinTable: string,\n rows: JoinRow[],\n preloads: Set<ManyToManyCollection<Entity, Entity>>,\n callback: (oldEntity: Entity, entities: Entity[]) => void,\n) {\n const api = getEmInternalApi(em);\n rows.forEach((row) => {\n // A join row's `columns` is always an object with two key/value pairs, one for each side of the m2m.\n // This only runs for entity-to-entity m2ms (enum m2ms are skipped by the caller), so both are entities.\n const [[col1, e1], [col2, e2]] = Object.entries(row.columns) as [[string, Entity], [string, Entity]];\n (\n [\n // Each join row needs to append/remove from both sides of the m2m. So we need to do the same operation once for\n // each side.\n [col1, e1, e2],\n [col2, e2, e1],\n ] as const\n ).forEach(([column, newEntity, newOther]) => {\n const { idTagged: id } = newEntity;\n const oldEntity = em.findExistingInstance(id)!;\n if (oldEntity.isDeletedEntity) return;\n const newM2m = getManyToManys(newEntity).find((r) => r.joinTableName === joinTable && r.columnName === column)!;\n const { fieldName } = newM2m;\n const oldM2m = (oldEntity as any)[fieldName] as ManyToManyCollection<Entity, Entity>;\n let entities: Entity[];\n if (preloads.has(oldM2m)) {\n // If a previous join row has already preloaded this relation, then we can just append/remove from the\n // existing preloaded data.\n entities = api.getPreloadedRelation(id, fieldName) as Entity[];\n } else if (oldM2m.isLoaded) {\n // The order here is important. M2ms are returned in the order their join rows were inserted, so we need to\n // make sure we match that behavior here. So we get all the extant rows first and then append each new row\n // in order from there. Removals are fine since they'll modify the array in place and the correct order will\n // be preserved.\n entities = oldM2m.get;\n } else if (newEntity.isNewEntity) {\n // If this is a new entity, then all the join rows referencing it must also be inserts. So we can just use an\n // empty array initially and inserts will be appended to it.\n entities = [];\n } else {\n fail(`Expected ${fieldName} to be loaded`);\n }\n callback(em.findExistingInstance(newOther.idTagged)!, entities);\n api.setPreloadedRelation(id, fieldName, entities);\n // We only need to preload the m2m once, but multiple join rows could reference it, so we keep track of which m2ms\n // need to be preloaded as we work, then actually call preload() at the end once we're done.\n preloads.add(oldM2m);\n });\n });\n}\n\n/** Returns the lazy columns on an entity, including inherited fields. */\nfunction getLazyFields(entity: Entity): IteratorObject<PrimitiveField> {\n return Object.values(getMetadata(entity).allFields)\n .values()\n .filter((field) => field.kind === \"primitive\")\n .filter((field) => !!field.lazy);\n}\n\nfunction getReferences(entity: Entity): IteratorObject<ConcreteReference> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToOneReferenceImpl ||\n r instanceof ReactiveReferenceImpl ||\n r instanceof PolymorphicReferenceImpl,\n );\n}\n\nfunction getManyToManys(\n entity: Entity,\n): IteratorObject<\n | ManyToManyCollection<Entity, Entity>\n | ReactiveManyToManyImpl<any, any, any>\n | ReactiveManyToManyOtherSideImpl<any, any>\n> {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl,\n );\n}\n\nfunction getCollections(entity: Entity) {\n return getRelations(entity)\n .values()\n .filter(\n (r) =>\n r instanceof ManyToManyCollection ||\n r instanceof OneToManyCollection ||\n r instanceof OneToOneReferenceImpl ||\n r instanceof ReactiveManyToManyImpl ||\n r instanceof ReactiveManyToManyOtherSideImpl ||\n r instanceof EnumCollectionImpl,\n );\n}\n"],"mappings":"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"}
@@ -68,7 +68,7 @@ function deepClone(obj, map = /* @__PURE__ */ new WeakMap()) {
68
68
  }
69
69
  /** Flattens/simplifies relations into their actual value, i.e. "calls get". */
70
70
  function maybeGetRelation(actualValue) {
71
- if ((0, joist_core.isReference)(actualValue) || (0, joist_core.isCollection)(actualValue) || (0, joist_core.isProperty)(actualValue) || (0, joist_core.isReactiveField)(actualValue) || (0, joist_core.isAsyncProperty)(actualValue)) return getWithSoftDeleted(actualValue);
71
+ if ((0, joist_core.isReference)(actualValue) || (0, joist_core.isCollection)(actualValue) || (0, joist_core.isProperty)(actualValue) || (0, joist_core.isLazyField)(actualValue) || (0, joist_core.isReactiveField)(actualValue) || (0, joist_core.isAsyncProperty)(actualValue)) return getWithSoftDeleted(actualValue);
72
72
  return actualValue;
73
73
  }
74
74
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"toMatchEntity.cjs","names":["isEntity","isPlainObject","isDefined","BaseEntity","isReference","isCollection","isProperty","isReactiveField","isAsyncProperty"],"sources":["../src/toMatchEntity.ts"],"sourcesContent":["import {\n BaseEntity,\n type Entity,\n type EntityManager,\n type Property,\n type ReadOnlyCollection,\n type Reference,\n isAsyncProperty,\n isCollection,\n isDefined,\n isEntity,\n isProperty,\n isReactiveField,\n isReference,\n} from \"joist-core\";\nimport { isPlainObject } from \"joist-utils\";\n\nimport { type CustomMatcherResult } from \"./index.ts\";\n\n// Vitest also populates the `$$jest-matchers-object` symbol, but the matchers it stores there are\n// chai-based and assume `this` is a chai Assertion (with a `this.assert` method), which our custom\n// matcher's `this` context is not--so calling them throws `this.assert is not a function`. Only take\n// the Jest fast-path under actual Jest, and route Vitest through the `this.equals` path below (like Bun).\nconst isVitest = typeof process !== \"undefined\" && process.env?.VITEST === \"true\";\n\n/**\n * Provides convenient `toMatchObject`-style matching for Joist entities.\n *\n * The biggest differences over `toMatchObject` are:\n *\n * - We un-wrap relations like m2o/o2m to just the underlying object/arrays, to make\n * asserting against the entities look like asserting against POJOs\n * - We prune the expected/actual values to be as minimal as possible, to avoid Jest\n * recursively crawling into connection pools or other misc non-useful things in the diffs\n */\nexport function toMatchEntity<T>(this: any, actual: unknown, expected: MatchedEntity<T>): CustomMatcherResult {\n // This module can load before Jest initializes its matcher state, so read the global when invoked.\n const matchers = isVitest ? undefined : (globalThis as any)[Symbol.for(\"$$jest-matchers-object\")]?.matchers;\n // We're given expected, which is an object literal of what the user wants to match\n // against (some intermixed POJOs & entities) and actual (which similarly could be\n // intermixed POJOs & entities, but will likely be more sprawling because it's the\n // full result, and not the subset we're asserting against).\n //\n // Given this, first make a safe clone of `expected`, i.e. turn entities into strings.\n // After that, use `expected` as a template to pick keys out of actual. Finally, do the\n // same \"clean clone\" of actual, and compare the two.\n const cleanExpected = deepClone(expected);\n const cleanActual = deepClone(deepMirror(cleanExpected, actual));\n\n // Watch for `expect(someAuthor).toMatchEntity(a1)` as we'll turn `someAuthor` into \"a:1\",\n // so can't use toMatchObject anymore.\n if (matchers) {\n // this is jest\n if (typeof cleanActual !== \"object\") {\n // @ts-ignore\n return matchers.toEqual.call(this, cleanActual, cleanExpected);\n } else {\n // @ts-ignore\n return matchers.toMatchObject.call(this, cleanActual, cleanExpected);\n }\n } else if (isVitest) {\n // `deepMirror` already pruned `cleanActual` down to the subset of keys that `cleanExpected`\n // declares, so a full `this.equals` deep-equality behaves like Jest's `toMatchObject`. We pass\n // `this.customTesters` so `areEntitiesEqual` still applies. Building the diff into `message`\n // (like Jest's `toMatchObject`) keeps the failure output readable both in the terminal and in\n // `toThrowErrorMatchingInlineSnapshot`, instead of the default `expect([object Object])...`.\n const pass = this.equals(cleanActual, cleanExpected, this.customTesters);\n return {\n pass,\n message: () => {\n const hint = this.utils.matcherHint(\"toMatchEntity\", undefined, undefined, { isNot: this.isNot });\n const diff = this.utils.diff(cleanExpected, cleanActual);\n return diff ? `${hint}\\n\\n${diff}` : hint;\n },\n };\n } else {\n // This is Bun, whose matcher `this.utils` only exposes `matcherHint`.\n return {\n pass: this.equals(cleanActual, cleanExpected),\n message: () => this.utils.matcherHint(\"toMatchEntity\", cleanActual, cleanExpected),\n };\n }\n}\n\nfunction maybeTestId(maybeEntity: any): any {\n return isEntity(maybeEntity) ? getTestId(maybeEntity.em, maybeEntity) : maybeEntity;\n}\n\n/** Returns either the persisted id or `tag#<offset-in-EntityManager>`. */\nfunction getTestId(_: EntityManager, entity: Entity): string {\n return entity.toTaggedString();\n}\n\n/**\n * Given a Joist entity `T`, \"flattens out\" the Reference/Collections.\n *\n * I.e. so that you can `toMatchEntity({ otherEntity: { name: \"foo\" } })` even though\n * `otherEntity` is technically a joist `Reference` or `Collection`.\n *\n * We allow `| null` so that `toMatchEntity` can work against optional fields\n * that are returned from GraphQL object resolvers.\n */\nexport type MatchedEntity<T> =\n T extends ReadonlyArray<infer U>\n ? ReadonlyArray<MatchedEntity<U>>\n :\n | undefined\n | T\n | {\n [K in keyof T]?: T[K] extends Reference<any, infer U, any>\n ? MatchedEntity<U> | U\n : T[K] extends ReadOnlyCollection<any, infer U>\n ? Array<MatchedEntity<U> | U>\n : T[K] extends Property<any, infer V>\n ? V\n : T[K] extends Entity | null | undefined\n ? MatchedEntity<T[K]> | T[K] | null | undefined\n : T[K] extends ReadonlyArray<(infer U) | undefined>\n ? readonly (MatchedEntity<U> | U | undefined)[]\n : T[K] extends ReadonlyArray<infer U> | null\n ? readonly (MatchedEntity<U> | U | null)[]\n : // We recurse similar to a DeepPartial\n MatchedEntity<T[K]> | null;\n };\n\n/**\n * Make a \"new actual\" based on the subset shape of expected.\n *\n * We don't have cycle detection, but that should be fine b/c the user types\n * out the expected as an object literal, so it shouldn't have cycles\n */\nfunction deepMirror(expected: any, actual: any): any {\n // If we've hit a point where the `expected` input is not a POJO object literal/array\n // declaring the subset of the shape to assert against, just return actual as-is.\n // This might be a Date (safe) or an Entity (unsafe) or something else, but if so it will\n // get cleaned up by the `deepClone` pass.\n if (!isPlainObject(expected) && !Array.isArray(expected)) return actual;\n if (!isDefined(actual)) return actual;\n // Make a new actual that is a subset that matches expected\n const subset: any = Array.isArray(expected) ? [] : ({} as any);\n // If both are arrays, fill out the whole array\n const keys = Array.isArray(actual) ? Object.keys(actual) : Object.keys(expected);\n for (const key of keys) {\n if (key in actual) {\n // Even if actualValue is an Entity, if expected has a key drilling in to it, we want to object literal-ize it\n subset[key] = deepMirror((expected as any)[key], maybeGetRelation(actual[key]));\n }\n }\n return subset;\n}\n\n/** Make a clone of `obj`, but only recurse into POJOs and Arrays, and replace entities if we find them. */\nfunction deepClone(obj: unknown, map = new WeakMap()): unknown {\n if (obj instanceof BaseEntity) {\n return maybeTestId(obj);\n } else if (obj && typeof obj === \"object\" && (isPlainObject(obj) || Array.isArray(obj))) {\n if (map.has(obj)) return map.get(obj);\n const result = Array.isArray(obj) ? [] : {};\n map.set(obj, result);\n Object.assign(result, ...Object.keys(obj).map((key) => ({ [key]: deepClone((obj as any)[key], map) })));\n return result;\n } else {\n return obj;\n }\n}\n\n/** Flattens/simplifies relations into their actual value, i.e. \"calls get\". */\nfunction maybeGetRelation(actualValue: unknown): unknown {\n if (\n isReference(actualValue) ||\n isCollection(actualValue) ||\n isProperty(actualValue) ||\n isReactiveField(actualValue) ||\n // hasAsyncPropertys will work if they've been already loaded, so let the user try it\n isAsyncProperty(actualValue)\n ) {\n return getWithSoftDeleted(actualValue);\n }\n return actualValue;\n}\n\n/**\n * Adds back soft-deleted to `.get`.\n *\n * In Joist, `.get` filters both soft and hard deleted entities, because `.get`\n * is likely used by real business logic that doesn't want to see either.\n *\n * However, in tests with `toMatchEntity`, we take a stronger stance that the\n * developer needs to know \"this is _soft_ deleted and not hard deleted\", so\n * we add-back soft deleted entities.\n *\n * We do this by calling `getWithDeleted`, which returns all hard and soft\n * deleted entities, and then filtering out the hard deletes.\n */\nfunction getWithSoftDeleted(relation: any): any {\n const r = \"getWithDeleted\" in relation ? relation.getWithDeleted : relation.get;\n return Array.isArray(r) ? r.filter((e: any) => !e.isDeletedEntity) : r;\n}\n"],"mappings":";;;;;AAuBA,MAAM,WAAW,OAAO,YAAY,eAAe,QAAQ,KAAK,WAAW;;;;;;;;;;;AAY3E,SAAgB,cAA4B,QAAiB,UAAiD;CAE5G,MAAM,WAAW,WAAW,KAAA,IAAa,WAAmB,OAAO,IAAI,wBAAwB,EAAE,EAAE;CASnG,MAAM,gBAAgB,UAAU,QAAQ;CACxC,MAAM,cAAc,UAAU,WAAW,eAAe,MAAM,CAAC;CAI/D,IAAI,UAAU;EAEZ,IAAI,OAAO,gBAAgB,UAEzB,OAAO,SAAS,QAAQ,KAAK,MAAM,aAAa,aAAa;OAG7D,OAAO,SAAS,cAAc,KAAK,MAAM,aAAa,aAAa;CAEvE,OAAO,IAAI,UAOT,OAAO;EACL,MAFW,KAAK,OAAO,aAAa,eAAe,KAAK,aAErD;EACH,eAAe;GACb,MAAM,OAAO,KAAK,MAAM,YAAY,iBAAiB,KAAA,GAAW,KAAA,GAAW,EAAE,OAAO,KAAK,MAAM,CAAC;GAChG,MAAM,OAAO,KAAK,MAAM,KAAK,eAAe,WAAW;GACvD,OAAO,OAAO,GAAG,KAAK,MAAM,SAAS;EACvC;CACF;MAGA,OAAO;EACL,MAAM,KAAK,OAAO,aAAa,aAAa;EAC5C,eAAe,KAAK,MAAM,YAAY,iBAAiB,aAAa,aAAa;CACnF;AAEJ;AAEA,SAAS,YAAY,aAAuB;CAC1C,QAAA,GAAOA,WAAAA,SAAAA,CAAS,WAAW,IAAI,UAAU,YAAY,IAAI,WAAW,IAAI;AAC1E;;AAGA,SAAS,UAAU,GAAkB,QAAwB;CAC3D,OAAO,OAAO,eAAe;AAC/B;;;;;;;AAwCA,SAAS,WAAW,UAAe,QAAkB;CAKnD,IAAI,EAAA,GAACC,YAAAA,cAAAA,CAAc,QAAQ,KAAK,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACjE,IAAI,EAAA,GAACC,WAAAA,UAAAA,CAAU,MAAM,GAAG,OAAO;CAE/B,MAAM,SAAc,MAAM,QAAQ,QAAQ,IAAI,CAAC,IAAK,CAAC;CAErD,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ;CAC/E,KAAK,MAAM,OAAO,MAChB,IAAI,OAAO,QAET,OAAO,OAAO,WAAY,SAAiB,MAAM,iBAAiB,OAAO,IAAI,CAAC;CAGlF,OAAO;AACT;;AAGA,SAAS,UAAU,KAAc,sBAAM,IAAI,QAAQ,GAAY;CAC7D,IAAI,eAAeC,WAAAA,YACjB,OAAO,YAAY,GAAG;MACjB,IAAI,OAAO,OAAO,QAAQ,cAAA,GAAaF,YAAAA,cAAAA,CAAc,GAAG,KAAK,MAAM,QAAQ,GAAG,IAAI;EACvF,IAAI,IAAI,IAAI,GAAG,GAAG,OAAO,IAAI,IAAI,GAAG;EACpC,MAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;EAC1C,IAAI,IAAI,KAAK,MAAM;EACnB,OAAO,OAAO,QAAQ,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK,SAAS,GAAG,MAAM,UAAW,IAAY,MAAM,GAAG,EAAE,EAAE,CAAC;EACtG,OAAO;CACT,OACE,OAAO;AAEX;;AAGA,SAAS,iBAAiB,aAA+B;CACvD,KAAA,GACEG,WAAAA,YAAAA,CAAY,WAAW,MAAA,GACvBC,WAAAA,aAAAA,CAAa,WAAW,MAAA,GACxBC,WAAAA,WAAAA,CAAW,WAAW,MAAA,GACtBC,WAAAA,gBAAAA,CAAgB,WAAW,MAAA,GAE3BC,WAAAA,gBAAAA,CAAgB,WAAW,GAE3B,OAAO,mBAAmB,WAAW;CAEvC,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,mBAAmB,UAAoB;CAC9C,MAAM,IAAI,oBAAoB,WAAW,SAAS,iBAAiB,SAAS;CAC5E,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,QAAQ,MAAW,CAAC,EAAE,eAAe,IAAI;AACvE"}
1
+ {"version":3,"file":"toMatchEntity.cjs","names":["isEntity","isPlainObject","isDefined","BaseEntity","isReference","isCollection","isProperty","isLazyField","isReactiveField","isAsyncProperty"],"sources":["../src/toMatchEntity.ts"],"sourcesContent":["import {\n BaseEntity,\n type Entity,\n type EntityManager,\n type Property,\n type ReadOnlyCollection,\n type Reference,\n isAsyncProperty,\n isCollection,\n isDefined,\n isEntity,\n isLazyField,\n isProperty,\n isReactiveField,\n isReference,\n} from \"joist-core\";\nimport { isPlainObject } from \"joist-utils\";\n\nimport { type CustomMatcherResult } from \"./index.ts\";\n\n// Vitest also populates the `$$jest-matchers-object` symbol, but the matchers it stores there are\n// chai-based and assume `this` is a chai Assertion (with a `this.assert` method), which our custom\n// matcher's `this` context is not--so calling them throws `this.assert is not a function`. Only take\n// the Jest fast-path under actual Jest, and route Vitest through the `this.equals` path below (like Bun).\nconst isVitest = typeof process !== \"undefined\" && process.env?.VITEST === \"true\";\n\n/**\n * Provides convenient `toMatchObject`-style matching for Joist entities.\n *\n * The biggest differences over `toMatchObject` are:\n *\n * - We un-wrap relations like m2o/o2m to just the underlying object/arrays, to make\n * asserting against the entities look like asserting against POJOs\n * - We prune the expected/actual values to be as minimal as possible, to avoid Jest\n * recursively crawling into connection pools or other misc non-useful things in the diffs\n */\nexport function toMatchEntity<T>(this: any, actual: unknown, expected: MatchedEntity<T>): CustomMatcherResult {\n // This module can load before Jest initializes its matcher state, so read the global when invoked.\n const matchers = isVitest ? undefined : (globalThis as any)[Symbol.for(\"$$jest-matchers-object\")]?.matchers;\n // We're given expected, which is an object literal of what the user wants to match\n // against (some intermixed POJOs & entities) and actual (which similarly could be\n // intermixed POJOs & entities, but will likely be more sprawling because it's the\n // full result, and not the subset we're asserting against).\n //\n // Given this, first make a safe clone of `expected`, i.e. turn entities into strings.\n // After that, use `expected` as a template to pick keys out of actual. Finally, do the\n // same \"clean clone\" of actual, and compare the two.\n const cleanExpected = deepClone(expected);\n const cleanActual = deepClone(deepMirror(cleanExpected, actual));\n\n // Watch for `expect(someAuthor).toMatchEntity(a1)` as we'll turn `someAuthor` into \"a:1\",\n // so can't use toMatchObject anymore.\n if (matchers) {\n // this is jest\n if (typeof cleanActual !== \"object\") {\n // @ts-ignore\n return matchers.toEqual.call(this, cleanActual, cleanExpected);\n } else {\n // @ts-ignore\n return matchers.toMatchObject.call(this, cleanActual, cleanExpected);\n }\n } else if (isVitest) {\n // `deepMirror` already pruned `cleanActual` down to the subset of keys that `cleanExpected`\n // declares, so a full `this.equals` deep-equality behaves like Jest's `toMatchObject`. We pass\n // `this.customTesters` so `areEntitiesEqual` still applies. Building the diff into `message`\n // (like Jest's `toMatchObject`) keeps the failure output readable both in the terminal and in\n // `toThrowErrorMatchingInlineSnapshot`, instead of the default `expect([object Object])...`.\n const pass = this.equals(cleanActual, cleanExpected, this.customTesters);\n return {\n pass,\n message: () => {\n const hint = this.utils.matcherHint(\"toMatchEntity\", undefined, undefined, { isNot: this.isNot });\n const diff = this.utils.diff(cleanExpected, cleanActual);\n return diff ? `${hint}\\n\\n${diff}` : hint;\n },\n };\n } else {\n // This is Bun, whose matcher `this.utils` only exposes `matcherHint`.\n return {\n pass: this.equals(cleanActual, cleanExpected),\n message: () => this.utils.matcherHint(\"toMatchEntity\", cleanActual, cleanExpected),\n };\n }\n}\n\nfunction maybeTestId(maybeEntity: any): any {\n return isEntity(maybeEntity) ? getTestId(maybeEntity.em, maybeEntity) : maybeEntity;\n}\n\n/** Returns either the persisted id or `tag#<offset-in-EntityManager>`. */\nfunction getTestId(_: EntityManager, entity: Entity): string {\n return entity.toTaggedString();\n}\n\n/**\n * Given a Joist entity `T`, \"flattens out\" the Reference/Collections.\n *\n * I.e. so that you can `toMatchEntity({ otherEntity: { name: \"foo\" } })` even though\n * `otherEntity` is technically a joist `Reference` or `Collection`.\n *\n * We allow `| null` so that `toMatchEntity` can work against optional fields\n * that are returned from GraphQL object resolvers.\n */\nexport type MatchedEntity<T> =\n T extends ReadonlyArray<infer U>\n ? ReadonlyArray<MatchedEntity<U>>\n :\n | undefined\n | T\n | {\n [K in keyof T]?: T[K] extends Reference<any, infer U, any>\n ? MatchedEntity<U> | U\n : T[K] extends ReadOnlyCollection<any, infer U>\n ? Array<MatchedEntity<U> | U>\n : T[K] extends Property<any, infer V>\n ? V\n : T[K] extends Entity | null | undefined\n ? MatchedEntity<T[K]> | T[K] | null | undefined\n : T[K] extends ReadonlyArray<(infer U) | undefined>\n ? readonly (MatchedEntity<U> | U | undefined)[]\n : T[K] extends ReadonlyArray<infer U> | null\n ? readonly (MatchedEntity<U> | U | null)[]\n : // We recurse similar to a DeepPartial\n MatchedEntity<T[K]> | null;\n };\n\n/**\n * Make a \"new actual\" based on the subset shape of expected.\n *\n * We don't have cycle detection, but that should be fine b/c the user types\n * out the expected as an object literal, so it shouldn't have cycles\n */\nfunction deepMirror(expected: any, actual: any): any {\n // If we've hit a point where the `expected` input is not a POJO object literal/array\n // declaring the subset of the shape to assert against, just return actual as-is.\n // This might be a Date (safe) or an Entity (unsafe) or something else, but if so it will\n // get cleaned up by the `deepClone` pass.\n if (!isPlainObject(expected) && !Array.isArray(expected)) return actual;\n if (!isDefined(actual)) return actual;\n // Make a new actual that is a subset that matches expected\n const subset: any = Array.isArray(expected) ? [] : ({} as any);\n // If both are arrays, fill out the whole array\n const keys = Array.isArray(actual) ? Object.keys(actual) : Object.keys(expected);\n for (const key of keys) {\n if (key in actual) {\n // Even if actualValue is an Entity, if expected has a key drilling in to it, we want to object literal-ize it\n subset[key] = deepMirror((expected as any)[key], maybeGetRelation(actual[key]));\n }\n }\n return subset;\n}\n\n/** Make a clone of `obj`, but only recurse into POJOs and Arrays, and replace entities if we find them. */\nfunction deepClone(obj: unknown, map = new WeakMap()): unknown {\n if (obj instanceof BaseEntity) {\n return maybeTestId(obj);\n } else if (obj && typeof obj === \"object\" && (isPlainObject(obj) || Array.isArray(obj))) {\n if (map.has(obj)) return map.get(obj);\n const result = Array.isArray(obj) ? [] : {};\n map.set(obj, result);\n Object.assign(result, ...Object.keys(obj).map((key) => ({ [key]: deepClone((obj as any)[key], map) })));\n return result;\n } else {\n return obj;\n }\n}\n\n/** Flattens/simplifies relations into their actual value, i.e. \"calls get\". */\nfunction maybeGetRelation(actualValue: unknown): unknown {\n if (\n isReference(actualValue) ||\n isCollection(actualValue) ||\n isProperty(actualValue) ||\n isLazyField(actualValue) ||\n isReactiveField(actualValue) ||\n // hasAsyncPropertys will work if they've been already loaded, so let the user try it\n isAsyncProperty(actualValue)\n ) {\n return getWithSoftDeleted(actualValue);\n }\n return actualValue;\n}\n\n/**\n * Adds back soft-deleted to `.get`.\n *\n * In Joist, `.get` filters both soft and hard deleted entities, because `.get`\n * is likely used by real business logic that doesn't want to see either.\n *\n * However, in tests with `toMatchEntity`, we take a stronger stance that the\n * developer needs to know \"this is _soft_ deleted and not hard deleted\", so\n * we add-back soft deleted entities.\n *\n * We do this by calling `getWithDeleted`, which returns all hard and soft\n * deleted entities, and then filtering out the hard deletes.\n */\nfunction getWithSoftDeleted(relation: any): any {\n const r = \"getWithDeleted\" in relation ? relation.getWithDeleted : relation.get;\n return Array.isArray(r) ? r.filter((e: any) => !e.isDeletedEntity) : r;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"toMatchEntity.d.cts","names":[],"sources":["../src/toMatchEntity.ts"],"mappings":";;;;;;;;;;;;;iBAmCgB,cAAc,GAAG,WAAW,iBAAiB,UAAU,cAAc,KAAK;;;;;;;;;;KAmE9E,cAAc,KACxB,UAAU,oBAAoB,KAC1B,cAAc,cAAc,kBAGxB,OAEG,WAAW,KAAK,EAAE,WAAW,qBAAqB,UAC/C,cAAc,KAAK,IACnB,EAAE,WAAW,8BAA8B,KACzC,MAAM,cAAc,KAAK,KACzB,EAAE,WAAW,oBAAoB,KAC/B,IACA,EAAE,WAAW,4BACX,cAAc,EAAE,MAAM,EAAE,wBACxB,EAAE,WAAW,qBAAqB,4BACtB,cAAc,KAAK,mBAC7B,EAAE,WAAW,oBAAoB,sBACrB,cAAc,KAAK;GAE7B,cAAc,EAAE"}
1
+ {"version":3,"file":"toMatchEntity.d.cts","names":[],"sources":["../src/toMatchEntity.ts"],"mappings":";;;;;;;;;;;;;iBAoCgB,cAAc,GAAG,WAAW,iBAAiB,UAAU,cAAc,KAAK;;;;;;;;;;KAmE9E,cAAc,KACxB,UAAU,oBAAoB,KAC1B,cAAc,cAAc,kBAGxB,OAEG,WAAW,KAAK,EAAE,WAAW,qBAAqB,UAC/C,cAAc,KAAK,IACnB,EAAE,WAAW,8BAA8B,KACzC,MAAM,cAAc,KAAK,KACzB,EAAE,WAAW,oBAAoB,KAC/B,IACA,EAAE,WAAW,4BACX,cAAc,EAAE,MAAM,EAAE,wBACxB,EAAE,WAAW,qBAAqB,4BACtB,cAAc,KAAK,mBAC7B,EAAE,WAAW,oBAAoB,sBACrB,cAAc,KAAK;GAE7B,cAAc,EAAE"}
@@ -1 +1 @@
1
- {"version":3,"file":"toMatchEntity.d.mts","names":[],"sources":["../src/toMatchEntity.ts"],"mappings":";;;;;;;;;;;;;iBAmCgB,cAAc,GAAG,WAAW,iBAAiB,UAAU,cAAc,KAAK;;;;;;;;;;KAmE9E,cAAc,KACxB,UAAU,oBAAoB,KAC1B,cAAc,cAAc,kBAGxB,OAEG,WAAW,KAAK,EAAE,WAAW,qBAAqB,UAC/C,cAAc,KAAK,IACnB,EAAE,WAAW,8BAA8B,KACzC,MAAM,cAAc,KAAK,KACzB,EAAE,WAAW,oBAAoB,KAC/B,IACA,EAAE,WAAW,4BACX,cAAc,EAAE,MAAM,EAAE,wBACxB,EAAE,WAAW,qBAAqB,4BACtB,cAAc,KAAK,mBAC7B,EAAE,WAAW,oBAAoB,sBACrB,cAAc,KAAK;GAE7B,cAAc,EAAE"}
1
+ {"version":3,"file":"toMatchEntity.d.mts","names":[],"sources":["../src/toMatchEntity.ts"],"mappings":";;;;;;;;;;;;;iBAoCgB,cAAc,GAAG,WAAW,iBAAiB,UAAU,cAAc,KAAK;;;;;;;;;;KAmE9E,cAAc,KACxB,UAAU,oBAAoB,KAC1B,cAAc,cAAc,kBAGxB,OAEG,WAAW,KAAK,EAAE,WAAW,qBAAqB,UAC/C,cAAc,KAAK,IACnB,EAAE,WAAW,8BAA8B,KACzC,MAAM,cAAc,KAAK,KACzB,EAAE,WAAW,oBAAoB,KAC/B,IACA,EAAE,WAAW,4BACX,cAAc,EAAE,MAAM,EAAE,wBACxB,EAAE,WAAW,qBAAqB,4BACtB,cAAc,KAAK,mBAC7B,EAAE,WAAW,oBAAoB,sBACrB,cAAc,KAAK;GAE7B,cAAc,EAAE"}
@@ -1,5 +1,5 @@
1
1
  import "./index.js";
2
- import { BaseEntity, isAsyncProperty, isCollection, isDefined, isEntity, isProperty, isReactiveField, isReference } from "joist-core";
2
+ import { BaseEntity, isAsyncProperty, isCollection, isDefined, isEntity, isLazyField, isProperty, isReactiveField, isReference } from "joist-core";
3
3
  import { isPlainObject } from "joist-utils";
4
4
  //#region src/toMatchEntity.ts
5
5
  const isVitest = typeof process !== "undefined" && process.env?.VITEST === "true";
@@ -67,7 +67,7 @@ function deepClone(obj, map = /* @__PURE__ */ new WeakMap()) {
67
67
  }
68
68
  /** Flattens/simplifies relations into their actual value, i.e. "calls get". */
69
69
  function maybeGetRelation(actualValue) {
70
- if (isReference(actualValue) || isCollection(actualValue) || isProperty(actualValue) || isReactiveField(actualValue) || isAsyncProperty(actualValue)) return getWithSoftDeleted(actualValue);
70
+ if (isReference(actualValue) || isCollection(actualValue) || isProperty(actualValue) || isLazyField(actualValue) || isReactiveField(actualValue) || isAsyncProperty(actualValue)) return getWithSoftDeleted(actualValue);
71
71
  return actualValue;
72
72
  }
73
73
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"toMatchEntity.js","names":[],"sources":["../src/toMatchEntity.ts"],"sourcesContent":["import {\n BaseEntity,\n type Entity,\n type EntityManager,\n type Property,\n type ReadOnlyCollection,\n type Reference,\n isAsyncProperty,\n isCollection,\n isDefined,\n isEntity,\n isProperty,\n isReactiveField,\n isReference,\n} from \"joist-core\";\nimport { isPlainObject } from \"joist-utils\";\n\nimport { type CustomMatcherResult } from \"./index.ts\";\n\n// Vitest also populates the `$$jest-matchers-object` symbol, but the matchers it stores there are\n// chai-based and assume `this` is a chai Assertion (with a `this.assert` method), which our custom\n// matcher's `this` context is not--so calling them throws `this.assert is not a function`. Only take\n// the Jest fast-path under actual Jest, and route Vitest through the `this.equals` path below (like Bun).\nconst isVitest = typeof process !== \"undefined\" && process.env?.VITEST === \"true\";\n\n/**\n * Provides convenient `toMatchObject`-style matching for Joist entities.\n *\n * The biggest differences over `toMatchObject` are:\n *\n * - We un-wrap relations like m2o/o2m to just the underlying object/arrays, to make\n * asserting against the entities look like asserting against POJOs\n * - We prune the expected/actual values to be as minimal as possible, to avoid Jest\n * recursively crawling into connection pools or other misc non-useful things in the diffs\n */\nexport function toMatchEntity<T>(this: any, actual: unknown, expected: MatchedEntity<T>): CustomMatcherResult {\n // This module can load before Jest initializes its matcher state, so read the global when invoked.\n const matchers = isVitest ? undefined : (globalThis as any)[Symbol.for(\"$$jest-matchers-object\")]?.matchers;\n // We're given expected, which is an object literal of what the user wants to match\n // against (some intermixed POJOs & entities) and actual (which similarly could be\n // intermixed POJOs & entities, but will likely be more sprawling because it's the\n // full result, and not the subset we're asserting against).\n //\n // Given this, first make a safe clone of `expected`, i.e. turn entities into strings.\n // After that, use `expected` as a template to pick keys out of actual. Finally, do the\n // same \"clean clone\" of actual, and compare the two.\n const cleanExpected = deepClone(expected);\n const cleanActual = deepClone(deepMirror(cleanExpected, actual));\n\n // Watch for `expect(someAuthor).toMatchEntity(a1)` as we'll turn `someAuthor` into \"a:1\",\n // so can't use toMatchObject anymore.\n if (matchers) {\n // this is jest\n if (typeof cleanActual !== \"object\") {\n // @ts-ignore\n return matchers.toEqual.call(this, cleanActual, cleanExpected);\n } else {\n // @ts-ignore\n return matchers.toMatchObject.call(this, cleanActual, cleanExpected);\n }\n } else if (isVitest) {\n // `deepMirror` already pruned `cleanActual` down to the subset of keys that `cleanExpected`\n // declares, so a full `this.equals` deep-equality behaves like Jest's `toMatchObject`. We pass\n // `this.customTesters` so `areEntitiesEqual` still applies. Building the diff into `message`\n // (like Jest's `toMatchObject`) keeps the failure output readable both in the terminal and in\n // `toThrowErrorMatchingInlineSnapshot`, instead of the default `expect([object Object])...`.\n const pass = this.equals(cleanActual, cleanExpected, this.customTesters);\n return {\n pass,\n message: () => {\n const hint = this.utils.matcherHint(\"toMatchEntity\", undefined, undefined, { isNot: this.isNot });\n const diff = this.utils.diff(cleanExpected, cleanActual);\n return diff ? `${hint}\\n\\n${diff}` : hint;\n },\n };\n } else {\n // This is Bun, whose matcher `this.utils` only exposes `matcherHint`.\n return {\n pass: this.equals(cleanActual, cleanExpected),\n message: () => this.utils.matcherHint(\"toMatchEntity\", cleanActual, cleanExpected),\n };\n }\n}\n\nfunction maybeTestId(maybeEntity: any): any {\n return isEntity(maybeEntity) ? getTestId(maybeEntity.em, maybeEntity) : maybeEntity;\n}\n\n/** Returns either the persisted id or `tag#<offset-in-EntityManager>`. */\nfunction getTestId(_: EntityManager, entity: Entity): string {\n return entity.toTaggedString();\n}\n\n/**\n * Given a Joist entity `T`, \"flattens out\" the Reference/Collections.\n *\n * I.e. so that you can `toMatchEntity({ otherEntity: { name: \"foo\" } })` even though\n * `otherEntity` is technically a joist `Reference` or `Collection`.\n *\n * We allow `| null` so that `toMatchEntity` can work against optional fields\n * that are returned from GraphQL object resolvers.\n */\nexport type MatchedEntity<T> =\n T extends ReadonlyArray<infer U>\n ? ReadonlyArray<MatchedEntity<U>>\n :\n | undefined\n | T\n | {\n [K in keyof T]?: T[K] extends Reference<any, infer U, any>\n ? MatchedEntity<U> | U\n : T[K] extends ReadOnlyCollection<any, infer U>\n ? Array<MatchedEntity<U> | U>\n : T[K] extends Property<any, infer V>\n ? V\n : T[K] extends Entity | null | undefined\n ? MatchedEntity<T[K]> | T[K] | null | undefined\n : T[K] extends ReadonlyArray<(infer U) | undefined>\n ? readonly (MatchedEntity<U> | U | undefined)[]\n : T[K] extends ReadonlyArray<infer U> | null\n ? readonly (MatchedEntity<U> | U | null)[]\n : // We recurse similar to a DeepPartial\n MatchedEntity<T[K]> | null;\n };\n\n/**\n * Make a \"new actual\" based on the subset shape of expected.\n *\n * We don't have cycle detection, but that should be fine b/c the user types\n * out the expected as an object literal, so it shouldn't have cycles\n */\nfunction deepMirror(expected: any, actual: any): any {\n // If we've hit a point where the `expected` input is not a POJO object literal/array\n // declaring the subset of the shape to assert against, just return actual as-is.\n // This might be a Date (safe) or an Entity (unsafe) or something else, but if so it will\n // get cleaned up by the `deepClone` pass.\n if (!isPlainObject(expected) && !Array.isArray(expected)) return actual;\n if (!isDefined(actual)) return actual;\n // Make a new actual that is a subset that matches expected\n const subset: any = Array.isArray(expected) ? [] : ({} as any);\n // If both are arrays, fill out the whole array\n const keys = Array.isArray(actual) ? Object.keys(actual) : Object.keys(expected);\n for (const key of keys) {\n if (key in actual) {\n // Even if actualValue is an Entity, if expected has a key drilling in to it, we want to object literal-ize it\n subset[key] = deepMirror((expected as any)[key], maybeGetRelation(actual[key]));\n }\n }\n return subset;\n}\n\n/** Make a clone of `obj`, but only recurse into POJOs and Arrays, and replace entities if we find them. */\nfunction deepClone(obj: unknown, map = new WeakMap()): unknown {\n if (obj instanceof BaseEntity) {\n return maybeTestId(obj);\n } else if (obj && typeof obj === \"object\" && (isPlainObject(obj) || Array.isArray(obj))) {\n if (map.has(obj)) return map.get(obj);\n const result = Array.isArray(obj) ? [] : {};\n map.set(obj, result);\n Object.assign(result, ...Object.keys(obj).map((key) => ({ [key]: deepClone((obj as any)[key], map) })));\n return result;\n } else {\n return obj;\n }\n}\n\n/** Flattens/simplifies relations into their actual value, i.e. \"calls get\". */\nfunction maybeGetRelation(actualValue: unknown): unknown {\n if (\n isReference(actualValue) ||\n isCollection(actualValue) ||\n isProperty(actualValue) ||\n isReactiveField(actualValue) ||\n // hasAsyncPropertys will work if they've been already loaded, so let the user try it\n isAsyncProperty(actualValue)\n ) {\n return getWithSoftDeleted(actualValue);\n }\n return actualValue;\n}\n\n/**\n * Adds back soft-deleted to `.get`.\n *\n * In Joist, `.get` filters both soft and hard deleted entities, because `.get`\n * is likely used by real business logic that doesn't want to see either.\n *\n * However, in tests with `toMatchEntity`, we take a stronger stance that the\n * developer needs to know \"this is _soft_ deleted and not hard deleted\", so\n * we add-back soft deleted entities.\n *\n * We do this by calling `getWithDeleted`, which returns all hard and soft\n * deleted entities, and then filtering out the hard deletes.\n */\nfunction getWithSoftDeleted(relation: any): any {\n const r = \"getWithDeleted\" in relation ? relation.getWithDeleted : relation.get;\n return Array.isArray(r) ? r.filter((e: any) => !e.isDeletedEntity) : r;\n}\n"],"mappings":";;;;AAuBA,MAAM,WAAW,OAAO,YAAY,eAAe,QAAQ,KAAK,WAAW;;;;;;;;;;;AAY3E,SAAgB,cAA4B,QAAiB,UAAiD;CAE5G,MAAM,WAAW,WAAW,KAAA,IAAa,WAAmB,OAAO,IAAI,wBAAwB,EAAE,EAAE;CASnG,MAAM,gBAAgB,UAAU,QAAQ;CACxC,MAAM,cAAc,UAAU,WAAW,eAAe,MAAM,CAAC;CAI/D,IAAI,UAAU;EAEZ,IAAI,OAAO,gBAAgB,UAEzB,OAAO,SAAS,QAAQ,KAAK,MAAM,aAAa,aAAa;OAG7D,OAAO,SAAS,cAAc,KAAK,MAAM,aAAa,aAAa;CAEvE,OAAO,IAAI,UAOT,OAAO;EACL,MAFW,KAAK,OAAO,aAAa,eAAe,KAAK,aAErD;EACH,eAAe;GACb,MAAM,OAAO,KAAK,MAAM,YAAY,iBAAiB,KAAA,GAAW,KAAA,GAAW,EAAE,OAAO,KAAK,MAAM,CAAC;GAChG,MAAM,OAAO,KAAK,MAAM,KAAK,eAAe,WAAW;GACvD,OAAO,OAAO,GAAG,KAAK,MAAM,SAAS;EACvC;CACF;MAGA,OAAO;EACL,MAAM,KAAK,OAAO,aAAa,aAAa;EAC5C,eAAe,KAAK,MAAM,YAAY,iBAAiB,aAAa,aAAa;CACnF;AAEJ;AAEA,SAAS,YAAY,aAAuB;CAC1C,OAAO,SAAS,WAAW,IAAI,UAAU,YAAY,IAAI,WAAW,IAAI;AAC1E;;AAGA,SAAS,UAAU,GAAkB,QAAwB;CAC3D,OAAO,OAAO,eAAe;AAC/B;;;;;;;AAwCA,SAAS,WAAW,UAAe,QAAkB;CAKnD,IAAI,CAAC,cAAc,QAAQ,KAAK,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACjE,IAAI,CAAC,UAAU,MAAM,GAAG,OAAO;CAE/B,MAAM,SAAc,MAAM,QAAQ,QAAQ,IAAI,CAAC,IAAK,CAAC;CAErD,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ;CAC/E,KAAK,MAAM,OAAO,MAChB,IAAI,OAAO,QAET,OAAO,OAAO,WAAY,SAAiB,MAAM,iBAAiB,OAAO,IAAI,CAAC;CAGlF,OAAO;AACT;;AAGA,SAAS,UAAU,KAAc,sBAAM,IAAI,QAAQ,GAAY;CAC7D,IAAI,eAAe,YACjB,OAAO,YAAY,GAAG;MACjB,IAAI,OAAO,OAAO,QAAQ,aAAa,cAAc,GAAG,KAAK,MAAM,QAAQ,GAAG,IAAI;EACvF,IAAI,IAAI,IAAI,GAAG,GAAG,OAAO,IAAI,IAAI,GAAG;EACpC,MAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;EAC1C,IAAI,IAAI,KAAK,MAAM;EACnB,OAAO,OAAO,QAAQ,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK,SAAS,GAAG,MAAM,UAAW,IAAY,MAAM,GAAG,EAAE,EAAE,CAAC;EACtG,OAAO;CACT,OACE,OAAO;AAEX;;AAGA,SAAS,iBAAiB,aAA+B;CACvD,IACE,YAAY,WAAW,KACvB,aAAa,WAAW,KACxB,WAAW,WAAW,KACtB,gBAAgB,WAAW,KAE3B,gBAAgB,WAAW,GAE3B,OAAO,mBAAmB,WAAW;CAEvC,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,mBAAmB,UAAoB;CAC9C,MAAM,IAAI,oBAAoB,WAAW,SAAS,iBAAiB,SAAS;CAC5E,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,QAAQ,MAAW,CAAC,EAAE,eAAe,IAAI;AACvE"}
1
+ {"version":3,"file":"toMatchEntity.js","names":[],"sources":["../src/toMatchEntity.ts"],"sourcesContent":["import {\n BaseEntity,\n type Entity,\n type EntityManager,\n type Property,\n type ReadOnlyCollection,\n type Reference,\n isAsyncProperty,\n isCollection,\n isDefined,\n isEntity,\n isLazyField,\n isProperty,\n isReactiveField,\n isReference,\n} from \"joist-core\";\nimport { isPlainObject } from \"joist-utils\";\n\nimport { type CustomMatcherResult } from \"./index.ts\";\n\n// Vitest also populates the `$$jest-matchers-object` symbol, but the matchers it stores there are\n// chai-based and assume `this` is a chai Assertion (with a `this.assert` method), which our custom\n// matcher's `this` context is not--so calling them throws `this.assert is not a function`. Only take\n// the Jest fast-path under actual Jest, and route Vitest through the `this.equals` path below (like Bun).\nconst isVitest = typeof process !== \"undefined\" && process.env?.VITEST === \"true\";\n\n/**\n * Provides convenient `toMatchObject`-style matching for Joist entities.\n *\n * The biggest differences over `toMatchObject` are:\n *\n * - We un-wrap relations like m2o/o2m to just the underlying object/arrays, to make\n * asserting against the entities look like asserting against POJOs\n * - We prune the expected/actual values to be as minimal as possible, to avoid Jest\n * recursively crawling into connection pools or other misc non-useful things in the diffs\n */\nexport function toMatchEntity<T>(this: any, actual: unknown, expected: MatchedEntity<T>): CustomMatcherResult {\n // This module can load before Jest initializes its matcher state, so read the global when invoked.\n const matchers = isVitest ? undefined : (globalThis as any)[Symbol.for(\"$$jest-matchers-object\")]?.matchers;\n // We're given expected, which is an object literal of what the user wants to match\n // against (some intermixed POJOs & entities) and actual (which similarly could be\n // intermixed POJOs & entities, but will likely be more sprawling because it's the\n // full result, and not the subset we're asserting against).\n //\n // Given this, first make a safe clone of `expected`, i.e. turn entities into strings.\n // After that, use `expected` as a template to pick keys out of actual. Finally, do the\n // same \"clean clone\" of actual, and compare the two.\n const cleanExpected = deepClone(expected);\n const cleanActual = deepClone(deepMirror(cleanExpected, actual));\n\n // Watch for `expect(someAuthor).toMatchEntity(a1)` as we'll turn `someAuthor` into \"a:1\",\n // so can't use toMatchObject anymore.\n if (matchers) {\n // this is jest\n if (typeof cleanActual !== \"object\") {\n // @ts-ignore\n return matchers.toEqual.call(this, cleanActual, cleanExpected);\n } else {\n // @ts-ignore\n return matchers.toMatchObject.call(this, cleanActual, cleanExpected);\n }\n } else if (isVitest) {\n // `deepMirror` already pruned `cleanActual` down to the subset of keys that `cleanExpected`\n // declares, so a full `this.equals` deep-equality behaves like Jest's `toMatchObject`. We pass\n // `this.customTesters` so `areEntitiesEqual` still applies. Building the diff into `message`\n // (like Jest's `toMatchObject`) keeps the failure output readable both in the terminal and in\n // `toThrowErrorMatchingInlineSnapshot`, instead of the default `expect([object Object])...`.\n const pass = this.equals(cleanActual, cleanExpected, this.customTesters);\n return {\n pass,\n message: () => {\n const hint = this.utils.matcherHint(\"toMatchEntity\", undefined, undefined, { isNot: this.isNot });\n const diff = this.utils.diff(cleanExpected, cleanActual);\n return diff ? `${hint}\\n\\n${diff}` : hint;\n },\n };\n } else {\n // This is Bun, whose matcher `this.utils` only exposes `matcherHint`.\n return {\n pass: this.equals(cleanActual, cleanExpected),\n message: () => this.utils.matcherHint(\"toMatchEntity\", cleanActual, cleanExpected),\n };\n }\n}\n\nfunction maybeTestId(maybeEntity: any): any {\n return isEntity(maybeEntity) ? getTestId(maybeEntity.em, maybeEntity) : maybeEntity;\n}\n\n/** Returns either the persisted id or `tag#<offset-in-EntityManager>`. */\nfunction getTestId(_: EntityManager, entity: Entity): string {\n return entity.toTaggedString();\n}\n\n/**\n * Given a Joist entity `T`, \"flattens out\" the Reference/Collections.\n *\n * I.e. so that you can `toMatchEntity({ otherEntity: { name: \"foo\" } })` even though\n * `otherEntity` is technically a joist `Reference` or `Collection`.\n *\n * We allow `| null` so that `toMatchEntity` can work against optional fields\n * that are returned from GraphQL object resolvers.\n */\nexport type MatchedEntity<T> =\n T extends ReadonlyArray<infer U>\n ? ReadonlyArray<MatchedEntity<U>>\n :\n | undefined\n | T\n | {\n [K in keyof T]?: T[K] extends Reference<any, infer U, any>\n ? MatchedEntity<U> | U\n : T[K] extends ReadOnlyCollection<any, infer U>\n ? Array<MatchedEntity<U> | U>\n : T[K] extends Property<any, infer V>\n ? V\n : T[K] extends Entity | null | undefined\n ? MatchedEntity<T[K]> | T[K] | null | undefined\n : T[K] extends ReadonlyArray<(infer U) | undefined>\n ? readonly (MatchedEntity<U> | U | undefined)[]\n : T[K] extends ReadonlyArray<infer U> | null\n ? readonly (MatchedEntity<U> | U | null)[]\n : // We recurse similar to a DeepPartial\n MatchedEntity<T[K]> | null;\n };\n\n/**\n * Make a \"new actual\" based on the subset shape of expected.\n *\n * We don't have cycle detection, but that should be fine b/c the user types\n * out the expected as an object literal, so it shouldn't have cycles\n */\nfunction deepMirror(expected: any, actual: any): any {\n // If we've hit a point where the `expected` input is not a POJO object literal/array\n // declaring the subset of the shape to assert against, just return actual as-is.\n // This might be a Date (safe) or an Entity (unsafe) or something else, but if so it will\n // get cleaned up by the `deepClone` pass.\n if (!isPlainObject(expected) && !Array.isArray(expected)) return actual;\n if (!isDefined(actual)) return actual;\n // Make a new actual that is a subset that matches expected\n const subset: any = Array.isArray(expected) ? [] : ({} as any);\n // If both are arrays, fill out the whole array\n const keys = Array.isArray(actual) ? Object.keys(actual) : Object.keys(expected);\n for (const key of keys) {\n if (key in actual) {\n // Even if actualValue is an Entity, if expected has a key drilling in to it, we want to object literal-ize it\n subset[key] = deepMirror((expected as any)[key], maybeGetRelation(actual[key]));\n }\n }\n return subset;\n}\n\n/** Make a clone of `obj`, but only recurse into POJOs and Arrays, and replace entities if we find them. */\nfunction deepClone(obj: unknown, map = new WeakMap()): unknown {\n if (obj instanceof BaseEntity) {\n return maybeTestId(obj);\n } else if (obj && typeof obj === \"object\" && (isPlainObject(obj) || Array.isArray(obj))) {\n if (map.has(obj)) return map.get(obj);\n const result = Array.isArray(obj) ? [] : {};\n map.set(obj, result);\n Object.assign(result, ...Object.keys(obj).map((key) => ({ [key]: deepClone((obj as any)[key], map) })));\n return result;\n } else {\n return obj;\n }\n}\n\n/** Flattens/simplifies relations into their actual value, i.e. \"calls get\". */\nfunction maybeGetRelation(actualValue: unknown): unknown {\n if (\n isReference(actualValue) ||\n isCollection(actualValue) ||\n isProperty(actualValue) ||\n isLazyField(actualValue) ||\n isReactiveField(actualValue) ||\n // hasAsyncPropertys will work if they've been already loaded, so let the user try it\n isAsyncProperty(actualValue)\n ) {\n return getWithSoftDeleted(actualValue);\n }\n return actualValue;\n}\n\n/**\n * Adds back soft-deleted to `.get`.\n *\n * In Joist, `.get` filters both soft and hard deleted entities, because `.get`\n * is likely used by real business logic that doesn't want to see either.\n *\n * However, in tests with `toMatchEntity`, we take a stronger stance that the\n * developer needs to know \"this is _soft_ deleted and not hard deleted\", so\n * we add-back soft deleted entities.\n *\n * We do this by calling `getWithDeleted`, which returns all hard and soft\n * deleted entities, and then filtering out the hard deletes.\n */\nfunction getWithSoftDeleted(relation: any): any {\n const r = \"getWithDeleted\" in relation ? relation.getWithDeleted : relation.get;\n return Array.isArray(r) ? r.filter((e: any) => !e.isDeletedEntity) : r;\n}\n"],"mappings":";;;;AAwBA,MAAM,WAAW,OAAO,YAAY,eAAe,QAAQ,KAAK,WAAW;;;;;;;;;;;AAY3E,SAAgB,cAA4B,QAAiB,UAAiD;CAE5G,MAAM,WAAW,WAAW,KAAA,IAAa,WAAmB,OAAO,IAAI,wBAAwB,EAAE,EAAE;CASnG,MAAM,gBAAgB,UAAU,QAAQ;CACxC,MAAM,cAAc,UAAU,WAAW,eAAe,MAAM,CAAC;CAI/D,IAAI,UAAU;EAEZ,IAAI,OAAO,gBAAgB,UAEzB,OAAO,SAAS,QAAQ,KAAK,MAAM,aAAa,aAAa;OAG7D,OAAO,SAAS,cAAc,KAAK,MAAM,aAAa,aAAa;CAEvE,OAAO,IAAI,UAOT,OAAO;EACL,MAFW,KAAK,OAAO,aAAa,eAAe,KAAK,aAErD;EACH,eAAe;GACb,MAAM,OAAO,KAAK,MAAM,YAAY,iBAAiB,KAAA,GAAW,KAAA,GAAW,EAAE,OAAO,KAAK,MAAM,CAAC;GAChG,MAAM,OAAO,KAAK,MAAM,KAAK,eAAe,WAAW;GACvD,OAAO,OAAO,GAAG,KAAK,MAAM,SAAS;EACvC;CACF;MAGA,OAAO;EACL,MAAM,KAAK,OAAO,aAAa,aAAa;EAC5C,eAAe,KAAK,MAAM,YAAY,iBAAiB,aAAa,aAAa;CACnF;AAEJ;AAEA,SAAS,YAAY,aAAuB;CAC1C,OAAO,SAAS,WAAW,IAAI,UAAU,YAAY,IAAI,WAAW,IAAI;AAC1E;;AAGA,SAAS,UAAU,GAAkB,QAAwB;CAC3D,OAAO,OAAO,eAAe;AAC/B;;;;;;;AAwCA,SAAS,WAAW,UAAe,QAAkB;CAKnD,IAAI,CAAC,cAAc,QAAQ,KAAK,CAAC,MAAM,QAAQ,QAAQ,GAAG,OAAO;CACjE,IAAI,CAAC,UAAU,MAAM,GAAG,OAAO;CAE/B,MAAM,SAAc,MAAM,QAAQ,QAAQ,IAAI,CAAC,IAAK,CAAC;CAErD,MAAM,OAAO,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,QAAQ;CAC/E,KAAK,MAAM,OAAO,MAChB,IAAI,OAAO,QAET,OAAO,OAAO,WAAY,SAAiB,MAAM,iBAAiB,OAAO,IAAI,CAAC;CAGlF,OAAO;AACT;;AAGA,SAAS,UAAU,KAAc,sBAAM,IAAI,QAAQ,GAAY;CAC7D,IAAI,eAAe,YACjB,OAAO,YAAY,GAAG;MACjB,IAAI,OAAO,OAAO,QAAQ,aAAa,cAAc,GAAG,KAAK,MAAM,QAAQ,GAAG,IAAI;EACvF,IAAI,IAAI,IAAI,GAAG,GAAG,OAAO,IAAI,IAAI,GAAG;EACpC,MAAM,SAAS,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC;EAC1C,IAAI,IAAI,KAAK,MAAM;EACnB,OAAO,OAAO,QAAQ,GAAG,OAAO,KAAK,GAAG,CAAC,CAAC,KAAK,SAAS,GAAG,MAAM,UAAW,IAAY,MAAM,GAAG,EAAE,EAAE,CAAC;EACtG,OAAO;CACT,OACE,OAAO;AAEX;;AAGA,SAAS,iBAAiB,aAA+B;CACvD,IACE,YAAY,WAAW,KACvB,aAAa,WAAW,KACxB,WAAW,WAAW,KACtB,YAAY,WAAW,KACvB,gBAAgB,WAAW,KAE3B,gBAAgB,WAAW,GAE3B,OAAO,mBAAmB,WAAW;CAEvC,OAAO;AACT;;;;;;;;;;;;;;AAeA,SAAS,mBAAmB,UAAoB;CAC9C,MAAM,IAAI,oBAAoB,WAAW,SAAS,iBAAiB,SAAS;CAC5E,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAE,QAAQ,MAAW,CAAC,EAAE,eAAe,IAAI;AACvE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "joist-test-utils",
3
- "version": "2.4.0-next.3",
3
+ "version": "2.4.0-next.31",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "build/index.cjs",
@@ -31,11 +31,11 @@
31
31
  "build"
32
32
  ],
33
33
  "dependencies": {
34
- "dotenv": "^17.4.2"
34
+ "dotenv": "^18.0.5"
35
35
  },
36
36
  "peerDependencies": {
37
- "joist-core": "2.4.0-next.3",
38
- "joist-utils": "2.4.0-next.3",
37
+ "joist-core": "2.4.0-next.31",
38
+ "joist-utils": "2.4.0-next.31",
39
39
  "pg": "^8.23.0"
40
40
  },
41
41
  "devDependencies": {