feat(sdks): add isomorphic spec platform

Adds an `isomorphic` virtual SDK platform whose spec is the union of the
`client` and `server` specs. Lets a single SDK release (e.g. the web
SDK) surface methods from both runtimes — including ones that are
hidden from the `server` spec or auth-excluded from it.

- `APP_SDK_PLATFORM_ISOMORPHIC` constant
- Specs.php emits `swagger2-<v>-isomorphic.json` (and OpenAPI3) using a
  union filter; `getKeys()` / `getAuthCounts()` extended accordingly
- SDKs.php honors an optional `specPlatform` per-SDK config override so
  a server-family SDK can point at the isomorphic spec
- Format/Swagger2 + OpenAPI3 use a shared `effectivePlatforms()` helper
  so the additional-methods filter expands isomorphic to client+server,
  preserving rename pairs like `createMfaAuthenticator` ↔
  `createMFAAuthenticator`
This commit is contained in:
Chirag Aggarwal
2026-05-19 15:18:35 +05:30
parent c5b8535a7f
commit da763bd55f
6 changed files with 56 additions and 8 deletions
+1
View File
@@ -103,6 +103,7 @@ const APP_SDK_PLATFORM_SERVER = 'server';
const APP_SDK_PLATFORM_CLIENT = 'client';
const APP_SDK_PLATFORM_CONSOLE = 'console';
const APP_SDK_PLATFORM_STATIC = 'static';
const APP_SDK_PLATFORM_ISOMORPHIC = 'isomorphic';
const APP_VCS_GITHUB_USERNAME = 'Appwrite';
const APP_VCS_GITHUB_EMAIL = 'team@appwrite.io';
const APP_VCS_GITHUB_URL = 'https://github.com/TeamAppwrite';
+2 -1
View File
@@ -308,7 +308,8 @@ class SDKs extends Action
} else {
Console::log(' Fetching API spec...');
$specPath = __DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $language['family'] . '.json';
$specPlatform = $language['specPlatform'] ?? $language['family'];
$specPath = __DIR__ . '/../../../../app/config/specs/swagger2-' . $version . '-' . $specPlatform . '.json';
if (!file_exists($specPath)) {
throw new \Exception('Spec file not found: ' . $specPath . '. Please run "docker compose exec appwrite specs --version=' . $version . '" first to generate the specs.');
+35 -5
View File
@@ -79,9 +79,28 @@ class Specs extends Action
APP_SDK_PLATFORM_CLIENT,
APP_SDK_PLATFORM_SERVER,
APP_SDK_PLATFORM_CONSOLE,
APP_SDK_PLATFORM_ISOMORPHIC,
];
}
/**
* Real platforms whose filters apply to a given (possibly virtual) platform.
*
* The isomorphic platform is a virtual union of `client` and `server` — its spec includes
* any route or service that would normally be emitted in either platform's spec. All other
* platforms map to themselves.
*
* @return array<string>
*/
private static function effectivePlatformsFor(string $platform): array
{
if ($platform === APP_SDK_PLATFORM_ISOMORPHIC) {
return [APP_SDK_PLATFORM_CLIENT, APP_SDK_PLATFORM_SERVER];
}
return [$platform];
}
/**
* Platforms to include in PR creation.
* Override in a subclass to exclude specific platforms.
@@ -121,6 +140,7 @@ class Specs extends Action
'client' => 1,
'server' => 2,
'console' => 1,
APP_SDK_PLATFORM_ISOMORPHIC => 1,
];
}
@@ -131,7 +151,7 @@ class Specs extends Action
*/
protected function getKeys(): array
{
return [
$keys = [
APP_SDK_PLATFORM_CLIENT => [
'Project' => [
'type' => 'apiKey',
@@ -325,6 +345,12 @@ class Specs extends Action
],
],
];
// Isomorphic platform shares the auth surface of both client and server runtimes; the
// generated SDK picks one auth path at construction time via its factories.
$keys[APP_SDK_PLATFORM_ISOMORPHIC] = $keys[APP_SDK_PLATFORM_CLIENT] + $keys[APP_SDK_PLATFORM_SERVER];
return $keys;
}
protected function verifyParsedSpec(array $spec): void
@@ -542,6 +568,7 @@ class Specs extends Action
$routes = [];
$models = [];
$services = [];
$effectivePlatforms = static::effectivePlatformsFor($platform);
foreach ($appRoutes as $key => $method) {
foreach ($method as $route) {
@@ -559,7 +586,10 @@ class Specs extends Action
/** @var Method $sdk */
$hide = $sdk->isHidden();
if ($hide === true || (\is_array($hide) && \in_array($platform, $hide))) {
// A method is hidden from a (possibly virtual) platform when it is hidden
// from every real platform that platform expands to. For real platforms
// this matches the legacy single-element check.
if ($hide === true || (\is_array($hide) && empty(\array_diff($effectivePlatforms, $hide)))) {
continue;
}
@@ -582,7 +612,7 @@ class Specs extends Action
continue;
}
if (!\in_array($platform, $sdkPlatforms)) {
if (empty(\array_intersect($effectivePlatforms, $sdkPlatforms))) {
continue;
}
@@ -601,8 +631,8 @@ class Specs extends Action
continue;
}
// Check if current platform is included in service's platforms
if (!\in_array($platform, $service['platforms'] ?? [])) {
// Check if any effective platform is included in service's platforms
if (empty(\array_intersect($effectivePlatforms, $service['platforms'] ?? []))) {
continue;
}
+16
View File
@@ -125,6 +125,22 @@ abstract class Format
$this->enumBlacklist = $this->buildEnumBlacklist();
}
/**
* Real platforms whose filters apply to this (possibly virtual) format platform.
*
* The isomorphic platform expands to client + server; all others map to themselves.
*
* @return array<string>
*/
protected function effectivePlatforms(): array
{
if ($this->platform === APP_SDK_PLATFORM_ISOMORPHIC) {
return [APP_SDK_PLATFORM_CLIENT, APP_SDK_PLATFORM_SERVER];
}
return [$this->platform];
}
protected function buildEnumBlacklist(): array
{
$blacklist = [];
@@ -172,7 +172,7 @@ class OpenAPI3 extends Format
$methodSecurities = $methodObj->getAuth();
$methodSdkPlatforms = $specs->getSDKPlatformsForRouteSecurity($methodSecurities);
if (!\in_array($this->platform, $methodSdkPlatforms)) {
if (empty(\array_intersect($this->effectivePlatforms(), $methodSdkPlatforms))) {
continue;
}
@@ -179,7 +179,7 @@ class Swagger2 extends Format
$methodSecurities = $methodObj->getAuth();
$methodSdkPlatforms = $specs->getSDKPlatformsForRouteSecurity($methodSecurities);
if (!\in_array($this->platform, $methodSdkPlatforms)) {
if (empty(\array_intersect($this->effectivePlatforms(), $methodSdkPlatforms))) {
continue;
}