Skip to content

fix(adapters): make the two shipped adapters agree - #59

Merged
Azerothian merged 1 commit into
mainfrom
fix/adapter-parity
Sep 1, 2026
Merged

fix(adapters): make the two shipped adapters agree#59
Azerothian merged 1 commit into
mainfrom
fix/adapter-parity

Conversation

@Azerothian

Copy link
Copy Markdown
Owner

Group 1 of the condense/dedupe/optimise pass. Three places the sequelize and valkey adapters were required to produce the same answer and quietly did not — each found by consolidating a near-duplicate and discovering the two copies had drifted.

These are correctness fixes, not tidying. A single-adapter test would have passed throughout the period they were broken, which is why the new tests run against both backends.

1. Enum types

sequelize valkey (before)
Type name TaskStatusEnum TaskstatusEnum
Member in-progress inProgress rejected by graphql

GraphQL names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/. A member like in-progress or 2xl was used verbatim as the enum value name and graphql refused it — and the throw landed nowhere near the cause: new GraphQLEnumType builds its values lazily, so construction succeeded and the failure surfaced wherever something first materialised them, naming assertEnumValueName rather than the definition that declared the member.

Both adapters now call createEnumType in @azerothian/graphql-types — the one GraphQL-aware package both already depend on (utilize is deliberately GraphQL-free, so it is the wrong home).

Only the schema-facing name is rewritten; the value reaching the backend is the member exactly as declared. Two members differing only in punctuation (in-progress / in progress) now raise a build-time error naming both, rather than collapsing into one value and leaving the loser unqueryable.

2. Bare JS constructors

type: String was accepted by valkey and passed straight through by sequelize into sequelize.define, where normalizeDataType does new Type() and yields a String wrapper object with no .key — not a valid Sequelize type, failing later in DDL generation rather than at define time.

The table now lives once as authoredDataType in @azerothian/utilize/types/data-type. Additive on sequelize — nothing that worked before stops working. Number maps to Int, not Float: JavaScript has one numeric type and no way to say which was meant, and silently widening an id column to floating point loses precision.

3. Relationship accessor names

getAssociations reported valkey's accessor names from the shared relationshipAccessors table, while tag() — which actually defines them — derived them a second way via a local helper and string interpolation. Two sources of truth for one set of names, in one file.

That helper's own docstring says exactly this must not happen: a cross-adapter relationship looks accessors up by the reported name and finds nothing if the two drift. They agreed by coincidence; nothing enforced it. tag() now uses the table, relNames is gone, and pluralize drops out of the package entirely.

Also

Renames temporalize's exported OrderEntrySortEntry. It collided with utilize's OrderEntry, a different and non-assignable shape on the barrel of a package temporalize depends on. The shapes differ for a good reason — the bare-string form is a convenience temporalize accepts and passes through — so the fix is one name each, not one type. Restated rather than imported: workflow-types.ts is bundled into the Temporal workflow sandbox and its header requires the module stay import-free.

Breaking

Documented in docs/migration-6-to-7.md with a members table:

  • valkey only — enum type names gain capitalisation (TaskstatusEnumTaskStatusEnum). A persisted schema artifact built against valkey must be rebuilt.
  • OrderEntrySortEntry in @azerothian/temporalize.

Verification

Confirmed the new tests are real pins — stashed the fix and re-ran: the bare-constructor case fails on sequelize, the enum-name case fails on valkey.

  • 1537 tests pass across all 9 packages (up from 1518; +19 new)
  • pnpm typecheck clean
  • pnpm lint clean at --max-warnings 0

🤖 Generated with Claude Code

https://claude.ai/code/session_019fGumVzMfMXDZ5vS7PJGDW

Three places the sequelize and valkey adapters were required to produce the same
answer and quietly did not. Each is a definition a caller could reasonably write
and expect to be portable; each was found by consolidating a near-duplicate and
discovering the two copies had drifted apart.

Enum types. The sequelize adapter capitalised the generated type name and
sanitised every member into a legal GraphQL name; the valkey adapter did neither.
So the same model produced `TaskstatusEnum` on one backend and `TaskStatusEnum`
on the other, and a member such as `in-progress` or `2xl` was rejected outright
by graphql — names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/. That throw landed
nowhere near the cause: `new GraphQLEnumType` builds its values lazily, so
construction succeeded and the failure surfaced wherever something first
materialised them, naming `assertEnumValueName` rather than the definition. Both
adapters now call `createEnumType` in `@azerothian/graphql-types` — the one
GraphQL-aware package both already depend on, since `utilize` is deliberately
GraphQL-free. Only the schema-facing name is rewritten; the value reaching the
backend is the member exactly as declared. Two members differing only in
punctuation now raise a build-time error naming both, instead of collapsing into
one value and leaving the loser unqueryable.

Bare constructors. `type: String` was accepted by the valkey adapter and passed
straight through by the sequelize one into `sequelize.define`, where
`normalizeDataType` does `new Type()` and yields a `String` wrapper object with
no `.key` — not a valid Sequelize type, failing later in DDL generation rather
than at define time. The token table now lives once, as `authoredDataType` in
`@azerothian/utilize/types/data-type`, and both adapters consult it. Additive on
sequelize: nothing that worked before stops working. `Number` maps to `Int`, not
`Float` — JavaScript has one numeric type and no way to say which was meant, and
silently widening an id column to floating point loses precision.

Accessor names. `getAssociations` reported valkey's relationship accessor names
from the shared `relationshipAccessors` table while `tag()`, which actually
defines them, derived them a second way through a local helper and string
interpolation. Two sources of truth for one set of names, in one file — exactly
what that helper's docstring says must not happen, because a cross-adapter
relationship looks accessors up by the reported name and finds nothing if the two
drift. They agreed by coincidence; nothing enforced it. `tag()` now uses the
table, `relNames` is gone, and `pluralize` drops out of the package entirely.

Also renames temporalize's exported `OrderEntry` to `SortEntry`. It collided with
utilize's `OrderEntry`, a different and non-assignable shape on the barrel of a
package temporalize depends on. The shapes differ for a good reason — the
bare-string spelling is a convenience this layer accepts and passes through — so
the fix is one name each, not one type. It is restated rather than imported
because `workflow-types.ts` is bundled into the Temporal workflow sandbox and its
header requires the module stay import-free.

The parity tests run against both backends, which is the point: a single-adapter
test would have passed throughout. Verified they fail without the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019fGumVzMfMXDZ5vS7PJGDW
@Azerothian
Azerothian merged commit 132eeb7 into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant