waaseyaa / graphql
GraphQL endpoint + schema introspection for Waaseyaa — optional/experimental L6 surface; see README for the primary JSON:API framing.
Requires
- php: >=8.5
- waaseyaa/access: ^0.1.0-alpha.256
- waaseyaa/api: ^0.1.0-alpha.256
- waaseyaa/entity: ^0.1.0-alpha.256
- waaseyaa/field: ^0.1.0-alpha.256
- waaseyaa/foundation: ^0.1.0-alpha.256
- waaseyaa/routing: ^0.1.0-alpha.256
- webonyx/graphql-php: ^15.31.5
Requires (Dev)
- phpunit/phpunit: ^10.5
This package is auto-updated.
Last update: 2026-07-06 21:32:24 UTC
README
Alternative protocol — not the primary API surface.
Per the framework's API-surface consolidation (mission
api-surface-consolidation-jsonapi-primary-01KSEFTV), the framework's primary API surface is JSON:API inpackages/api/.waaseyaa/graphqlremains supported as an optional / experimental L6 protocol adapter for distributions whose consumers need GraphQL. It is not bundled bywaaseyaa/full; install it explicitly when your distribution chooses GraphQL.
Layer 6 — Interfaces
GraphQL endpoint for Waaseyaa with auto-generated schema from registered entity types.
GraphQlEndpoint accepts queries at the configured route (registered via GraphQlRouteProvider) and resolves them against EntityTypeManagerInterface-derived schemas. Connection-style pagination follows the Relay spec: totalCount reflects the full unfiltered dataset (matching JSON:API semantics — see #436), while items returns only the access-filtered subset. Field resolvers honour FieldAccessPolicyInterface so attribute-level access control matches the JSON:API surface.
Key classes: GraphQlEndpoint, GraphQlRouteProvider, GraphQlServiceProvider.
Status
- Stability: optional / experimental. The public API surface (
GraphQlServiceProvider, the/graphqlendpoint, the schema-loading mechanism, any documented resolvers / mutations) is frozen at its current shape. The framework cadence ships no new feature work for this package; community contributions are accepted under the same review bar. - Bundle membership: suggested by
waaseyaa/full(not required). To install:composer require waaseyaa/graphql. - Decision provenance: API-surface consolidation by mission
api-surface-consolidation-jsonapi-primary-01KSEFTV. JSON:API is declared the framework's primary API surface indocs/specs/jsonapi.md.
Implementation gotchas
- Reference fields keep storage field names: A field defined as
author_idwith typeentity_referenceproduces a GraphQL field namedauthor_id(notauthor). It resolves to the nested entity object but the field name includes the_idsuffix. - List filter/sort fields are gated through field-level access (R14, audit A11):
EntityResolver::resolveList()applies caller-supplied filter/sort arguments as raw storage conditions. Previouslytotalanditemswere gated only by the entity-levelguard->canView()predicate, so a field restricted per row by a dynamicFieldAccessPolicy(a classification/clearance field) was a presence/ordering oracle: filteringfilter: [{field: "classification_field", value: "secret"}]returned that value's row count even though the caller could not read the field.resolveList()now excludes a row from BOTH the count loop and the item loop when any caller-supplied filter/sort field is view-Forbiddenfor it (GraphQlAccessGuard::isFieldViewForbidden()), value-independently (dropped because the caller may not READ the queried field, never because of its value), matching the RESTJsonApiController::index()fix. BecauseQueryApplierruns sort+pagination in storage before that drop, a sort on a field view-Forbiddenon any viewable matched row is additionally REJECTED (EntityResolver::rejectForbiddenSort()throws aUserError), so a Forbidden row can never occupy an observable pagination rank (the empty-vs-populated-page ordering oracle). Gated to the bound-account path; the system-context bypass keeps the raw storageCOUNT. Structural allowlist (R15, audit A11): the residual the R14 entry flagged is now closed —EntityResolver::assertQueryableFields()runs at the top ofresolveList()(before any storage query, unconditionally) and throws aUserErrorfor any filter/sort field that is not a declared field or entity key, is inALWAYS_INTERNAL_FIELDS(pass/password/password_hash), or is a declared field flaggedsettings['internal'] => true. This mirrors REST'sJsonApiController::validateQueryFields()and closes the undeclared-_data-key oracle (which reachedjson_extract('$.<field>')) and theinternal-flagged-secret oracle (e.g.User.two_factor_secret), both of which R14's per-policy gate could not see. Seedocs/specs/api-layer.md"Field-access gate on filter/sort fields (audit R14)". Pinned byEntityResolverFieldFilterOracleTest(R14) andEntityResolverStructuralFieldAllowlistTest(R15). - Mutations require an authenticated account (R11):
GraphQlEndpoint::handle()rejects any mutation operation (create{Type}/update{Type}/delete{Type}, any alias oroperationName-selected mutation) for an unauthenticated (AccountInterface::isAuthenticated() === false) caller, for every HTTP method, BEFORE building the schema or invoking a resolver: a uniform error message"Authentication required for mutation operations."(no entity id/type ever named) and the mutation never executes. Queries are unaffected. This closes an anonymous existence oracle:update{Type}/delete{Type}distinguished "entity absent" ("Entity not found: {type}/{id}") from "entity exists but access denied", an anonymous or otherwise-unauthorized caller could enumerate entity ids by diffing the two messages even though every per-entityAccessPolicyInterfacewas itself correct. As defense-in-depth for the authenticated-but-unauthorized case (not blocked by the gate above),EntityResolver::resolveUpdate()/resolveDelete()now collapse an access-denied outcome, at BOTH the entity level AND the per-fieldeditlevel (theassertFieldEditAccess()loop is inside the collapse), into the SAME "Entity not found" error the absent-entity branch throws, mirroringresolveSingle(), which has always returnednulluniformly for both cases. (The endpoint setsstatusCode401 internally, butGraphQlRouter::handle()currently hardcodes an HTTP-200 envelope regardless, a separate pre-existing issue, out of R11 scope.) Seedocs/specs/api-layer.md(2026-07-05 entry) for the full writeup.