-
Notifications
You must be signed in to change notification settings - Fork 58
feat(orm): map entities onto collections #947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feat-query-lib
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Database\ORM; | ||
|
|
||
| use Utopia\Database\ORM\Mapping\Column; | ||
|
|
||
| class ColumnMapping | ||
| { | ||
| public function __construct( | ||
| public readonly string $propertyName, | ||
| public readonly string $documentKey, | ||
| public readonly Column $column, | ||
| ) { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Database\ORM; | ||
|
|
||
| class EmbeddableMapping | ||
| { | ||
| public function __construct( | ||
| public readonly string $propertyName, | ||
| public readonly string $typeName, | ||
| public readonly string $prefix, | ||
| ) { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| <?php | ||
|
|
||
| namespace Utopia\Database\ORM; | ||
|
|
||
| use Utopia\Database\Database; | ||
| use Utopia\Database\Document; | ||
| use Utopia\Database\Query; | ||
|
|
||
| class EntityManager | ||
| { | ||
| private UnitOfWork $unitOfWork; | ||
|
|
||
| private IdentityMap $identityMap; | ||
|
|
||
| private MetadataFactory $metadataFactory; | ||
|
|
||
| private EntityMapper $entityMapper; | ||
|
|
||
| private Database $db; | ||
|
|
||
| public function __construct(Database $db) | ||
| { | ||
| $this->db = $db; | ||
| $this->identityMap = new IdentityMap(); | ||
| $this->metadataFactory = new MetadataFactory(); | ||
| $this->entityMapper = new EntityMapper($this->metadataFactory); | ||
| $this->unitOfWork = new UnitOfWork( | ||
| $this->identityMap, | ||
| $this->metadataFactory, | ||
| $this->entityMapper, | ||
| ); | ||
| } | ||
|
|
||
| public function persist(object $entity): void | ||
| { | ||
| $this->unitOfWork->persist($entity); | ||
| } | ||
|
|
||
| public function remove(object $entity): void | ||
| { | ||
| $this->unitOfWork->remove($entity); | ||
| } | ||
|
|
||
| public function forceRemove(object $entity): void | ||
| { | ||
| $this->unitOfWork->forceRemove($entity); | ||
| } | ||
|
|
||
| public function restore(object $entity): void | ||
| { | ||
| $this->unitOfWork->restore($entity); | ||
| } | ||
|
|
||
| public function flush(): void | ||
| { | ||
| $this->unitOfWork->flush($this->db); | ||
| } | ||
|
|
||
| /** | ||
| * @template T of object | ||
| * @param class-string<T> $className | ||
| * @return T|null | ||
| */ | ||
| public function find(string $className, string $id, bool $withTrashed = false): ?object | ||
| { | ||
| $metadata = $this->metadataFactory->getMetadata($className); | ||
|
|
||
| $existing = $this->identityMap->get($metadata->collection, $id); | ||
| if ($existing !== null) { | ||
| /** @var T $existing */ | ||
| return $existing; | ||
| } | ||
|
|
||
| $document = $this->db->getDocument($metadata->collection, $id); | ||
|
Comment on lines
+68
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an ID belongs to a soft-deleted entity, Prompt To Fix With AIThis is a comment left during a code review.
Path: src/Database/ORM/EntityManager.php
Line: 68-74
Comment:
**Soft-deleted entities remain visible**
When an ID belongs to a soft-deleted entity, `find()` returns it from the identity map or loads it through `getDocument()` without applying the soft-delete filter, causing ID lookups to expose records that `findMany()` and `findOne()` hide by default.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
|
|
||
| if ($document->isEmpty()) { | ||
| return null; | ||
| } | ||
|
|
||
| // findMany() hides soft-deleted rows unless asked for them. Looking one up | ||
| // by id has to hide them too, or the same record is absent from a listing | ||
| // and present from a direct fetch. | ||
| if ( | ||
| ! $withTrashed | ||
| && $metadata->softDeleteColumn !== null | ||
| && $document->getAttribute($metadata->softDeleteColumn) !== null | ||
| ) { | ||
| return null; | ||
| } | ||
|
|
||
| /** @var T $entity */ | ||
| $entity = $this->entityMapper->toEntity($document, $metadata, $this->identityMap); | ||
| $this->unitOfWork->registerManaged($entity, $metadata); | ||
|
|
||
| return $entity; | ||
| } | ||
|
|
||
| /** | ||
| * @template T of object | ||
| * @param class-string<T> $className | ||
| * @param array<Query> $queries | ||
| * @return array<T> | ||
| */ | ||
| public function findMany(string $className, array $queries = [], bool $withTrashed = false): array | ||
| { | ||
| $metadata = $this->metadataFactory->getMetadata($className); | ||
|
|
||
| if (! $withTrashed && $metadata->softDeleteColumn !== null) { | ||
| $queries[] = Query::isNull($metadata->softDeleteColumn); | ||
| } | ||
|
|
||
| $documents = $this->db->find($metadata->collection, $queries); | ||
| $entities = []; | ||
|
|
||
| foreach ($documents as $document) { | ||
| /** @var T $entity */ | ||
| $entity = $this->entityMapper->toEntity($document, $metadata, $this->identityMap); | ||
| $this->unitOfWork->registerManaged($entity, $metadata); | ||
| $entities[] = $entity; | ||
| } | ||
|
|
||
| return $entities; | ||
| } | ||
|
|
||
| /** | ||
| * @template T of object | ||
| * @param class-string<T> $className | ||
| * @param array<Query> $queries | ||
| * @return T|null | ||
| */ | ||
| public function findOne(string $className, array $queries = []): ?object | ||
| { | ||
| $queries[] = Query::limit(1); | ||
| $results = $this->findMany($className, $queries); | ||
|
|
||
| if ($results === []) { | ||
| return null; | ||
| } | ||
|
|
||
| /** @var T */ | ||
| return $results[0]; | ||
| } | ||
|
|
||
| public function createCollectionFromEntity(string $className): Document | ||
| { | ||
| $metadata = $this->metadataFactory->getMetadata($className); | ||
| $defs = $this->entityMapper->toCollectionDefinitions($metadata); | ||
|
|
||
| /** @var \Utopia\Database\Collection $collection */ | ||
| $collection = $defs['collection']; | ||
| /** @var array<\Utopia\Database\Relationship> $relationships */ | ||
| $relationships = $defs['relationships']; | ||
|
|
||
| $doc = $this->db->createCollection($collection); | ||
|
|
||
| foreach ($relationships as $relationship) { | ||
| $this->db->createRelationship($relationship); | ||
| } | ||
|
|
||
| return $doc; | ||
| } | ||
|
|
||
| public function syncCollectionFromEntity(string $className): void | ||
| { | ||
| $metadata = $this->metadataFactory->getMetadata($className); | ||
| $defs = $this->entityMapper->toCollectionDefinitions($metadata); | ||
|
|
||
| /** @var \Utopia\Database\Collection $desired */ | ||
| $desired = $defs['collection']; | ||
|
|
||
| if (! $this->db->exists($this->db->getAdapter()->getDatabase(), $metadata->collection)) { | ||
| $this->createCollectionFromEntity($className); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| $current = $this->db->getCollection($metadata->collection); | ||
|
|
||
| $differ = new \Utopia\Database\Schema\Diff(); | ||
| $diff = $differ->diff($current, $desired); | ||
|
Comment on lines
+166
to
+180
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an existing collection's relationship annotations are added, removed, or changed, this branch applies only the collection attribute/index diff and ignores Knowledge Base Used: Collection schema management Prompt To Fix With AIThis is a comment left during a code review.
Path: src/Database/ORM/EntityManager.php
Line: 155-169
Comment:
**Relationship synchronization is omitted**
When an existing collection's relationship annotations are added, removed, or changed, this branch applies only the collection attribute/index diff and ignores `defs['relationships']`, leaving relationship metadata and backend structures missing or stale and potentially treating existing relationship attributes as invalid attribute removals.
**Knowledge Base Used:** [Collection schema management](https://app.greptile.com/appwrite/-/custom-context/knowledge-base/utopia-php/database/-/docs/collection-schema-management.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
|
|
||
| if ($diff->hasChanges()) { | ||
| $diff->apply($this->db, $metadata->collection); | ||
| } | ||
| } | ||
|
|
||
| public function detach(object $entity): void | ||
| { | ||
| $this->unitOfWork->detach($entity); | ||
| } | ||
|
|
||
| public function clear(): void | ||
| { | ||
| $this->unitOfWork->clear(); | ||
| } | ||
|
|
||
| public function getUnitOfWork(): UnitOfWork | ||
| { | ||
| return $this->unitOfWork; | ||
| } | ||
|
|
||
| public function getIdentityMap(): IdentityMap | ||
| { | ||
| return $this->identityMap; | ||
| } | ||
|
|
||
| public function getMetadataFactory(): MetadataFactory | ||
| { | ||
| return $this->metadataFactory; | ||
| } | ||
|
|
||
| public function getEntityMapper(): EntityMapper | ||
| { | ||
| return $this->entityMapper; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the same
EntityManagerfirst loads a soft-deleted entity withwithTrashed=trueand then performs a default lookup for that ID,find()returns the identity-mapped instance before reaching the soft-delete check, causing the default lookup to expose an entity that default listings exclude.Knowledge Base Used: Document lifecycle and representation
Prompt To Fix With AI