Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@
"@statsig/session-replay": "^3.33.0",
"@statsig/statsig-node-core": "^0.19.3",
"@statsig/web-analytics": "^3.33.0",
"@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f",
"h3": "^1.15.4",
"sharp": "^0.34.5"
},
"devDependencies": {
"@appwrite.io/console": "^0.6.4",
"@appwrite.io/pink": "~0.26.0",
"@appwrite.io/pink-icons": "~0.26.0",
"@appwrite.io/specs": "github:appwrite/specs#095d750d7036626d949279ddb7cd9510b902a13f",
"@eslint/compat": "^1.4.1",
"@eslint/js": "^9.39.1",
"@fingerprintjs/fingerprintjs": "^4.6.2",
Expand Down Expand Up @@ -117,7 +117,6 @@
"typescript-eslint": "^8.48.1",
"vaul-svelte": "1.0.0-next.7",
"vite": "^7.2.7",
"vite-plugin-dynamic-import": "^1.6.0",
"vite-plugin-image-optimizer": "^2.0.3",
"vite-plugin-manifest-sri": "^0.2.0",
"vitest": "^3.2.4",
Expand Down
18 changes: 18 additions & 0 deletions scripts/build.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
import { build } from 'vite';
import { cp, mkdir } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

async function main() {
await build();

// Copy @appwrite.io/specs data into the build output so it ships with the
// deployment archive and is reachable at runtime even in environments that
// don't include node_modules (e.g. Appwrite Sites runtime, where only the
// build artifact is mounted).
const require = createRequire(import.meta.url);
const specsRoot = dirname(require.resolve('@appwrite.io/specs/package.json'));
const projectRoot = dirname(fileURLToPath(import.meta.url)).replace(/\/scripts$/, '');
const target = resolve(projectRoot, 'build/_specs_data');

await mkdir(target, { recursive: true });
await cp(resolve(specsRoot, 'specs'), resolve(target, 'specs'), { recursive: true });
await cp(resolve(specsRoot, 'examples'), resolve(target, 'examples'), { recursive: true });
console.log('[build] copied specs data to', target);
}

main();
188 changes: 107 additions & 81 deletions src/routes/docs/references/[version]/[platform]/[service]/specs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,62 @@
import { error } from '@sveltejs/kit';
import { OpenAPIV3 } from 'openapi-types';
import { Platform, type ServiceValue, type Version } from '$lib/utils/references';
import { Platform, VALID_PLATFORMS, versions, type ServiceValue } from '$lib/utils/references';
import { existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

// Spec data location. In environments where `@appwrite.io/specs` is installed
// (local dev, Docker prod with `bun install --production`), resolve via Node
// module resolution. In environments where only the build artifact ships
// (Appwrite Sites runtime), fall back to `_specs_data/` copied next to the
// server bundle by `scripts/build.js`.
function locateSpecsRoot(): string {
try {
const fromNodeModules = dirname(
createRequire(import.meta.url).resolve('@appwrite.io/specs/package.json')
);
if (existsSync(join(fromNodeModules, 'specs'))) {
return fromNodeModules;
}
} catch {
// package not installed at runtime; fall through to bundled data
}

const here = dirname(fileURLToPath(import.meta.url));
const candidates = [
resolve(here, '../../_specs_data'),
resolve(here, '../_specs_data'),
resolve(process.cwd(), '_specs_data')
];
for (const candidate of candidates) {
if (existsSync(join(candidate, 'specs'))) {
return candidate;
}
}
throw new Error('Unable to locate @appwrite.io/specs data');
}

const specsRoot = locateSpecsRoot();

// URL segments reach this module from SvelteKit route params, so anything
// interpolated into a filesystem path needs an explicit allowlist check.
const VALID_VERSIONS = new Set<string>(versions as readonly string[]);

function assertValidVersion(version: string): void {
if (!VALID_VERSIONS.has(version)) {
throw error(404, `Unknown spec version ${version}`);
}
Comment thread
TorstenDittmann marked this conversation as resolved.
}

function assertValidPlatform(platform: string): void {
if (!VALID_PLATFORMS.has(platform as Platform)) {
throw error(404, `Unknown platform ${platform}`);
}
}

const apiCache = new Map<string, OpenAPIV3.Document>();

export type SDKMethod = {
'rate-limit': number;
Expand Down Expand Up @@ -90,62 +146,15 @@ export const ModelType = {
type ModelTypeType = keyof typeof ModelType;
type ModelTypeValue = (typeof ModelType)[ModelTypeType];

type ExampleVersion = Exclude<Version, 'cloud'>;
type ExampleLoaders = Record<string, () => Promise<unknown>>;

const examplesByVersion: Record<ExampleVersion, ExampleLoaders> = {
'0.15.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/0.15.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.0.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.0.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.1.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.1.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.2.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.2.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.3.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.3.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.4.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.4.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.5.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.5.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.6.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.6.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.7.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.7.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.8.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.8.x/**/*.md', {
query: '?raw',
import: 'default'
}),
'1.9.x': import.meta.glob('/node_modules/@appwrite.io/specs/examples/1.9.x/**/*.md', {
query: '?raw',
import: 'default'
})
};

function getExamples(version: string) {
if (!(version in examplesByVersion)) {
return undefined;
async function loadExample(relativePath: string): Promise<string | null> {
try {
return await readFile(join(specsRoot, relativePath), 'utf8');
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
return null;
}
throw e;
}

return examplesByVersion[version as ExampleVersion];
}

function stripMarkdownCodeFence(content: string): string {
Expand Down Expand Up @@ -396,23 +405,36 @@ export function getSchema(id: string, api: OpenAPIV3.Document): OpenAPIV3.Schema
error(404, { message: `Not found` });
}

const specs = import.meta.glob('/node_modules/@appwrite.io/specs/specs/*/open-api3*.json', {
exhaustive: true
});

export async function getApi(version: string, platform: string): Promise<OpenAPIV3.Document> {
// Only `version` reaches the filesystem path here; `platform` is collapsed to
// a fixed `mode` literal (server/client/console). Platform validation lives
// in `getService`, where the raw value is interpolated into the example path.
assertValidVersion(version);

const cacheKey = `${version}|${platform}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}

const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const filename = `open-api3-${version}-${mode}.json`;
Comment on lines +414 to 422
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The apiCache key includes the full platform string (e.g. server-nodejs, server-python), but the JSON file actually read is determined by mode — one of three fixed literals (server, client, console). Every server-side platform for the same version maps to the same open-api3-${version}-server.json file, so each distinct platform string creates a separate cache entry that parses and stores an identical multi-MB document. On a live SSR server where different users request server-nodejs, server-python, server-php, etc. concurrently, the same JSON is parsed and allocated once per platform variant — partially undoing the memory benefit of the cache. The key should be ${version}|${mode} so all server platforms share one entry.

Suggested change
const cacheKey = `${version}|${platform}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}
const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const filename = `open-api3-${version}-${mode}.json`;
const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const cacheKey = `${version}|${mode}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}
const filename = `open-api3-${version}-${mode}.json`;


const loader = Object.entries(specs).find(([key]) => key.endsWith(`/${filename}`))?.[1];

if (!loader) {
throw error(404, `Missing OpenAPI spec loader for ${filename}`);
const specPath = join(specsRoot, 'specs', version, filename);

let raw: string;
try {
raw = await readFile(specPath, 'utf8');
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'ENOENT') {
throw error(404, `Missing OpenAPI spec ${filename}`);
}
throw e;
}

const loaded = (await loader()) as OpenAPIV3.Document | { default: OpenAPIV3.Document };
return ('default' in loaded ? loaded.default : loaded) as OpenAPIV3.Document;
const parsed = JSON.parse(raw) as OpenAPIV3.Document;
apiCache.set(cacheKey, parsed);
return parsed;
}

const descriptions = import.meta.glob('./descriptions/*.md', {
Expand Down Expand Up @@ -441,6 +463,10 @@ export async function getService(
};
methods: SDKMethod[];
}> {
// `platform` is interpolated into example paths below, so validate against
// the public allowlist before any filesystem access.
assertValidPlatform(platform);

/**
* Exceptions for Android SDK.
*/
Expand All @@ -460,13 +486,7 @@ export async function getService(
methods: []
};

const examples = getExamples(version);

if (!examples) {
return data;
}

for (const { method, value, url } of iterateAllMethods(api, service)) {
const prepared = Array.from(iterateAllMethods(api, service)).map(({ method, value, url }) => {
const operation = value as AppwriteOperationObject;
const parameters = getParameters(operation);
const responses: SDKMethod['responses'] = Object.entries(operation.responses ?? {}).map(
Expand Down Expand Up @@ -507,22 +527,28 @@ export async function getService(
}
);

const path = isAndroid
? `/node_modules/@appwrite.io/specs/examples/${version}/${
const examplePath = isAndroid
? `examples/${version}/${
isAndroidServer ? 'server-kotlin' : 'client-android'
}/${isAndroidJava ? 'java' : 'kotlin'}/${operation['x-appwrite']?.demo}`
: `/node_modules/@appwrite.io/specs/examples/${version}/${platform}/examples/${operation['x-appwrite']?.demo}`;
: `examples/${version}/${platform}/examples/${operation['x-appwrite']?.demo}`;

if (!(path in examples)) {
return { method, value: operation, url, parameters, responses, examplePath };
});

const demos = await Promise.all(prepared.map((p) => loadExample(p.examplePath)));

for (let i = 0; i < prepared.length; i++) {
const demo = demos[i];
if (demo === null) {
continue;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

const demo = (await examples[path]()) as unknown as string;
const { method, value: operation, url, parameters, responses } = prepared[i];

data.methods.push({
id: operation['x-appwrite'].method,
group: operation['x-appwrite'].group,
demo: typeof demo === 'string' ? stripMarkdownCodeFence(demo) : '',
demo: stripMarkdownCodeFence(demo),
title: operation.summary ?? '',
description: operation.description ?? '',
parameters: parameters ?? [],
Expand Down
8 changes: 0 additions & 8 deletions vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { enhancedImages } from '@sveltejs/enhanced-img';
import { sveltekit } from '@sveltejs/kit/vite';
import dynamicImport from 'vite-plugin-dynamic-import';
import { ViteImageOptimizer } from 'vite-plugin-image-optimizer';
import manifestSRI from 'vite-plugin-manifest-sri';
import { defineConfig } from 'vitest/config';
Expand All @@ -21,13 +20,6 @@ export default defineConfig({
// }),
enhancedImages(),
sveltekit(),
dynamicImport({
filter(id) {
if (id.includes('/node_modules/@appwrite.io/specs/examples')) {
return true;
}
}
}),
ViteImageOptimizer({
include: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg'],
exclude: ['**/*.avif', '**/*.webp'],
Expand Down
Loading