feat(organisations): project the organisation as an object so a $ref can reach it - #3363
Open
rubenvdlinde wants to merge 1 commit into
Open
feat(organisations): project the organisation as an object so a $ref can reach it#3363rubenvdlinde wants to merge 1 commit into
rubenvdlinde wants to merge 1 commit into
Conversation
…can reach it
Several apps grew their own `organization` SCHEMA, and measuring one
showed why. `publication.organization` and `catalog.organization` are
declared as `{"format": "uuid", "$ref": "organization"}`. A $ref resolves
against a SCHEMA, and OpenRegister's Organisation is an ENTITY with no
object projection, so there was nothing for that reference to point at
and each app declared its own copy. A schema slug is global per
organisation, so those copies collide: `organization` is claimed by both
opencatalogi and stackiq today.
Adding the identity columns made reuse possible at the entity level and
`organisations:adopt` moves the rows. Neither gives a leaf schema
something to reference. This does.
Not a new mechanism: OpenRegister already projects nc-user and nc-group
exactly this way, and nc-group is even mapped to schema:Organization.
The provider follows GroupObjectSourceProvider line for line, including
the rule that absent and denied are indistinguishable. The schema is
nc-prefixed for the reason the semantic map already gives for the
app-gated rows: it must not collide with the leaf `organization` schemas
it exists to replace, which keep working until each app migrates off.
Three deliberate limits. It is READ-ONLY, because the authoritative
record is the Organisation row and a write path here would be a second
way to mutate a tenant that bypasses the organisation lifecycle. It
carries the IDENTITY FACET ONLY, because quota, users, groups and
authorization are tenancy administration and this schema exists so a
record can reference an organisation rather than configure one. And it is
SCOPED: an admin sees every organisation, anyone else only the ones they
belong to, anonymous callers nothing, so the projection cannot be used to
discover which tenants an instance has.
A merged-away organisation is not listed, because it owns nothing and
offering it invites a reference to an unusable target. find() still
resolves THROUGH a merge, so a reference stored before one keeps working.
One defect the tests caught and reading did not: project() first used
method_exists($organisation, 'getName') before calling the getter.
Organisation's accessors are magic, so method_exists is false for every
one of them and the projection would have shipped carrying nothing but an
id — a schema that resolves, returns objects, and is empty.
Verified live: the object API returns the organisation with its identity
facet, the same call unauthenticated returns total 0 rather than an
error, find by uuid works, and seeding on a clean row creates the schema
WITH its properties and links it to the directory register.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Several apps grew their own
organizationschema, and measuring one showed why.publication.organizationandcatalog.organizationare declared as{"type": "string", "format": "uuid", "$ref": "organization"}. A$refresolves against a schema, and OpenRegister's Organisation is an entity with no object projection — so there was nothing for that reference to point at, and each app declared its own copy instead.A schema slug is global per organisation (
SchemaMapper::find()matchesLOWER(slug)across every app and returns the first row it reaches), so those copies collide.organizationis claimed by both opencatalogi and stackiq today.Adding the identity columns to Organisation made reuse possible at the entity level, and
openregister:organisations:adopt(#3340) moves the rows. Neither gives a leaf schema something to reference. This is the missing piece.Not a new mechanism
OpenRegister already projects
nc-userandnc-groupexactly this way — andnc-groupis even mapped toschema:Organization.OrganisationObjectSourceProviderfollowsGroupObjectSourceProviderline for line: the same read-only contract, the same acting-user scoping, the same "absent and denied are indistinguishable" rule.The schema is
nc--prefixed for the reason the semantic map already gives for its app-gated rows: it must not collide with the leaf-apporganizationschemas it exists to replace, which have to keep working until each app has migrated off them.Three deliberate limits
OrganisationService's lifecycle.A merged-away organisation is not listed — it owns nothing, and offering it invites a reference to an unusable target.
find()still resolves through a merge, so a reference stored before one keeps working.A defect the tests caught and reading did not
project()was first written withmethod_exists($organisation, 'getName')before calling the getter. Organisation's accessors are magic (Entity::__call), somethod_existsis false for every one of them — the projection would have shipped carrying nothing but an id. A schema that resolves, returns objects, and is empty.Verification
Live, on a throwaway instance:
GET /api/objects/{directory}/{nc-organisation}as admintotal: 0— not an error, not a rowfindby uuid10 unit tests pin the projected set, the omissions (quota/users/groups/authorization), the merge exclusion, search, and the anonymous case. A script asserts the provider's projected list and the seeder's declared properties agree, rather than eyeballing it — a property declared and not projected reads as permanently empty, and one projected and not declared is discarded by the store without a word.
Full-tree phpstan clean. phpcs 0 errors, phpmd clean.
tests/Unit/Service/ObjectSource: 115 tests green.One thing worth knowing
ensureSchema()reuses an existing schema and never updates it, by design. A property added toSCHEMA_PROPERTIEStherefore does not reach an instance that already seeded that schema — verified by deleting the row and re-running the repair step. Any future property change to a virtual schema needs its own migration. Recorded in the change's tasks.What this unblocks
opencatalogi can now repoint
publication.organization/catalog.organizationatnc-organisationand retire its own copy. Stackiq can follow, once the nine properties Organisation has no column for get a home.🤖 Generated with Claude Code