Koncepcje, Idee, Strategie
Koncepcje, Idee, StrategieStrategie wersjonowania pól i dyrektyw

Strategie wersjonowania pól i dyrektyw

Przeczytaj najpierw przewodnik Rozwijanie schematu poprzez wersjonowanie pól, który wyjaśnia funkcję "field versioning" w Gato GraphQL.

Gato GraphQL pozwala polom i dyrektywom przyjmować argument versionConstraint, aby wybrać konkretną wersję (czyli implementację) pola/dyrektywy do użycia:

query GetPosts {
  posts(versionConstraint: "^1.0") {
    id
    title(versionConstraint: ">=2.1")
    excerpt @strUpperCase(versionConstraint: "~1.5.3")
  }
}

Co powinno się stać, gdy nie podamy argumentu versionConstraint? Na przykład, do której wersji powinno zostać rozwiązane pole surname w poniższej query?

query GetSurname {
  account(id: 1) {
    # Która wersja powinna być użyta? 1.0.0? 2.0.0?
    surname
  }
}

Mamy tutaj dwie kwestie do rozważenia:

  1. Ustalenie, która wersja jest domyślna, gdy żadna nie została podana
  2. Poinformowanie klienta, że istnieje kilka wersji do wyboru

Zanim zajmiemy się tymi kwestiami, musimy sprawdzić, jak dobrze GraphQL zapewnia kontekstowe informacje zwrotne podczas wykonywania queries.

Dostarczanie kontekstowych informacji zwrotnych podczas wykonywania queries

Musimy wskazać pewną mniej niż idealną sytuację w GraphQL: nie oferuje on dobrych informacji kontekstowych podczas wykonywania queries. Jest to wyraźnie widoczne w odniesieniu do przestarzałości (deprecations), gdzie dane o przestarzałości są wyświetlane tylko poprzez introspekcję przez odpytywanie pól isDeprecated i deprecationReason na typach Field i Enum:

{
  __type(name: "Account") {
    name
    fields {
      name
      isDeprecated
      deprecationReason
    }
  }
}

Odpowiedź będzie wyglądać następująco:

{
  "data": {
    "__type": {
      "name": "Account",
      "fields": [
        {
          "name": "id",
          "isDeprecated": false,
          "deprecationReason": null
        },
        {
          "name": "name",
          "isDeprecated": false,
          "deprecationReason": null
        },
        {
          "name": "surname",
          "isDeprecated": true,
          "deprecationReason": "Use `personSurname`"
        },
        {
          "name": "personSurname",
          "isDeprecated": false,
          "deprecationReason": null
        }
      ]
    }
  }
}

Jednak podczas wykonywania query obejmującej przestarzałe pole…

query GetSurname {
  account(id: 1) {
    surname
  }
}

...informacje o przestarzałości nie pojawią się w odpowiedzi:

{
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

Oznacza to, że programista wykonujący query musi aktywnie uruchamiać queries introspekcji, aby dowiedzieć się, czy schemat został zaktualizowany i czy jakieś pole zostało uznane za przestarzałe. Może to się zdarzyć… raz na jakiś czas? Najprawdopodobniej nigdy?

Znaczącą poprawą w zakresie aktualizowania przestarzałych queries byłoby, gdyby API GraphQL dostarczało informacje o przestarzałości podczas wykonywania queries obejmujących przestarzałe pola. Idealnie, informacje te byłyby podawane w nowym wpisie najwyższego poziomu deprecations, pojawiającym się po errors i przed data (zgodnie z sugestią specyfikacji dotyczącą formatu odpowiedzi).

Ponieważ wpis najwyższego poziomu deprecations nie jest częścią specyfikacji, funkcja "Proactive Feedback" w Gato GraphQL dodaje obsługę lepszych informacji zwrotnych w odpowiedzi na query, używając ogólnego wpisu najwyższego poziomu extensions, który pozwala na rozszerzanie protokołu w razie potrzeby:

Informacje o przestarzałości w odpowiedzi na query

Ogłaszanie wersji poprzez ostrzeżenia

Właśnie dowiedzieliśmy się, że serwer GraphQL może używać wpisu najwyższego poziomu extensions do dostarczania informacji o przestarzałości. Możemy użyć tej samej metodologii, aby dodać wpis warnings, w którym informujemy programistę, że pole zostało poddane wersjonowaniu. Nie dostarczamy tych informacji zawsze; tylko wtedy, gdy query obejmuje pole, które zostało poddane wersjonowaniu, a argument versionConstraint jest nieobecny.

Definiowanie domyślnej wersji dla pola

Istnieje kilka podejść, które możemy zastosować, w tym:

  1. Uczynienie versionConstraint obowiązkowym
  2. Używanie starej wersji domyślnie do określonej daty, po której nowa wersja staje się domyślna
  3. Używanie najnowszej wersji domyślnie i zachęcanie programistów queries do jawnego wskazania, której wersji użyć

Przeanalizujmy każdą z tych strategii i zobaczmy ich odpowiedzi podczas wykonywania tej query:

query GetSurname {
  account(id: 1) {
    surname
  }
}

1. Uczynienie versionConstraint obowiązkowym

Jest to najbardziej oczywiste rozwiązanie: zabronić klientowi niepodawania ograniczenia wersji poprzez uczynienie argumentu pola obowiązkowym. Wtedy, gdy nie zostanie podany, query zwróci błąd.

Wykonanie query odpowie następująco:

{
  "errors": [
    {
      "message": "Argument 'versionConstraint' in field 'surname' cannot be empty"
    }
  ],
  "data": {
    "account": {
      "surname": null
    }
  }
}

2. Używanie starej wersji domyślnie do określonej daty, po której nowa wersja staje się domyślna

Kontynuowanie używania starej wersji do określonej daty, kiedy nowa wersja stanie się domyślna. W tym okresie przejściowym należy prosić programistów queries, aby jawnie dodali ograniczenie wersji dla starej wersji przed tą datą, poprzez nowy wpis extensions.warnings w query.

Wykonanie query mogłoby odpowiedzieć następująco:

{
  "extensions": {
    "warnings": [
      {
        "message": "Field 'surname' has a new version: '2.0.0'. This version will become the default one on January 1st. We advise you to use this new version already and test that it works fine; if you find any problem, please report the issue in https://github.com/mycompany/myproject/issues. To do the switch, please add the 'versionConstraint' field argument to your query (using Composer's semver constraint rules; see https://getcomposer.org/doc/articles/versions.md#writing-version-constraints): surname(versionConstraint:\"^2.0\"). If you are unable to switch to the new version, please make sure to explicitly point to the current version '1.0.0' before January 1st: surname(versionConstraint:\"^1.0\"). In case of doubt, please contact us at name@company.com.",
    ]
  },
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

3. Używanie najnowszej wersji i zachęcanie użytkowników do jawnego wskazania, której wersji użyć

Używanie najnowszej wersji pola, gdy versionConstraint nie jest ustawiony, i zachęcanie programistów queries do jawnego zdefiniowania, której wersji należy użyć, wyświetlając listę wszystkich dostępnych wersji dla danego pola poprzez nowy wpis extensions.warnings:

Wykonanie query mogłoby odpowiedzieć następująco:

{
  "extensions": {
    "warnings": [
      {
        "message": "Field 'surname' has more than 1 version. Please add the 'versionConstraint' field argument to your query to indicate which version to use (using Composer's semver constraint rules; see https://getcomposer.org/doc/articles/versions.md#writing-version-constraints). To use the latest version, use: surname(versionConstraint:\"^2.0\"). Available versions: '2.0.0', '1.0.0'.",
    ]
  },
  "data": {
    "account": {
      "surname": "Owens"
    }
  }
}

Wersjonowanie dyrektyw

Możemy używać tych samych strategii do wersjonowania dyrektyw. Na przykład, wykonując query bez podania ograniczenia wersji:

query {
  post(by: { id: 1 }) {
    title @strTitleCase
  }
}

Może ona przyjąć domyślną wersję do użycia i wygenerować komunikat ostrzegawczy dla programisty, aby przejrzał query:

Odpytywanie wersjonowanej dyrektywy bez ograniczeń wersji