@zeroin.earth/appwrite-ts
v1.0.2
Published
TypeScript Server Plugin + CLI that generates fully typed collections from your Appwrite database schema
Maintainers
Readme
@zeroin.earth/appwrite-ts
A TypeScript Server Plugin + CLI that generates fully typed collections from your Appwrite database schema. Point it at your appwrite.config.json and get const-typed declarations, then use the exported utility types to build type-safe wrappers for your data layer.
How It Works
Pull your Appwrite project config using the Appwrite CLI:
appwrite pull all # or just the parts you need: appwrite pull collectionsThis writes (or updates)
appwrite.config.jsonin your project root, which includes atablesarray describing every collection and its columns.The TSS plugin (or CLI) reads that JSON and generates an
appwrite-config.d.tsfile with const-typed literal tuples.You import
typeof appwriteConfigand pass it as aConfigparameter to the library's utility types —CollectionType,AppwriteDocument, etc. — to get precise, per-collection field types.
Whenever your Appwrite schema changes, run appwrite pull again. The TSS plugin watches the file and regenerates the declaration automatically; if you're using the CLI, re-run appwrite-ts generate.
Setup
bun add @zeroin.earth/appwrite-tsPeer dependency: typescript ^5
TypeScript Server Plugin
Register the plugin in tsconfig.json so your editor regenerates the declaration file automatically whenever appwrite.config.json changes:
{
"compilerOptions": {
"plugins": [
{
"name": "@zeroin.earth/appwrite-ts/plugin",
"configPath": "./appwrite.config.json",
"outputPath": "./appwrite-config.d.ts"
}
]
}
}| Option | Description | Default |
|---|---|---|
| configPath | Path to the Appwrite config JSON | ./appwrite.config.json |
| outputPath | Path for the generated .d.ts file | ./appwrite-config.d.ts |
VS Code setup: Make sure VS Code is using your workspace TypeScript (not its bundled copy). Add this to .vscode/settings.json:
{
"js/ts.tsdk.path": "node_modules/typescript/lib"
}Then select the workspace TypeScript version: open any .ts file, click the TypeScript version in the bottom-right status bar, and choose "Use Workspace Version".
After initial setup, restart TS Server once (Cmd/Ctrl+Shift+P → "TypeScript: Restart TS Server"). The plugin will then watch for changes automatically — no restart needed after appwrite pull.
CLI
You can also generate the declaration file manually or in CI:
# One-shot generation
appwrite-ts generate ./appwrite.config.json -o ./appwrite-config.d.ts
# Watch mode — regenerates on every change to the config file
appwrite-ts watch ./appwrite.config.json -o ./appwrite-config.d.tsExported Types
CollectionType<Id, Config?>
Maps a collection $id to an object type of its fields. Pass your literal Config for precise types; omit it for a generic fallback.
AppwriteDocument<Id, Config?>
AppwriteMeta & CollectionType<Id, Config> — a fully-typed document including Appwrite system fields.
AppwriteRow
AppwriteMeta & { readonly [key: string]: unknown } — an untyped row with an index signature, useful for generic operations like iterating keys or stripping metadata.
AppwriteMeta
Appwrite system fields present on every document:
| Field | Type |
|---|---|
| $id | string |
| $createdAt | string |
| $updatedAt | string |
| $permissions | string[] |
| $databaseId | string |
| $tableId | string |
| $sequence | number (optional) |
Column, Table, AppwriteConfig
Runtime shapes for the config JSON. Used internally by the generator and available for tooling that needs to inspect the schema at runtime.
Column Type Mapping
| Appwrite column type | TypeScript type |
|---|---|
| boolean | boolean |
| integer, double | number |
| varchar, text, mediumtext, string | string |
| string with format: "enum" | Union of literal element strings |
| Any column with array: true | T[] |
| Any column with required: false | T \| null |
Usage
Binding Your Config
The core pattern: import the generated declaration and pass its type as the Config parameter.
import type appwriteConfig from './appwrite-config'
import type { AppwriteDocument, CollectionType } from '@zeroin.earth/appwrite-ts'
// Bind the literal config
type Config = typeof appwriteConfig
// Extract the union of all collection IDs
type CollectionId = Config['tables'][number]['$id']
// → "users" | "posts" | "comments" | ...
// Get precise field types for a specific collection
type PostFields = CollectionType<'posts', Config>
// → { title: string; body: string; authorId: string; published: boolean; views: number | null }
// Get a full Appwrite document (system fields + collection fields)
type PostDoc = AppwriteDocument<'posts', Config>
// → AppwriteMeta & PostFieldsBuilding Typed Wrappers
In practice, you'll want to build a small set of wrapper types in your project that bind Config once and give you convenient aliases for the rest of your codebase. This is where the real value is — you define these once and every data-access function, mapper, and persister gets full type safety for free.
// types.ts — define once, import everywhere
import type appwriteConfig from './appwrite-config'
import type { AppwriteDocument, AppwriteMeta } from '@zeroin.earth/appwrite-ts'
type Config = typeof appwriteConfig
/** Union of all collection $id strings. */
export type CollectionId = Config['tables'][number]['$id']
/** A fully-typed Appwrite document for a given collection. */
export type Doc<Id extends CollectionId> = AppwriteDocument<Id, Config>
/** An untyped row with index signature for generic operations (stripping meta, etc). */
export type UntypedRow = AppwriteMeta & { [key: string]: unknown }Now you can use Doc<'posts'>, Doc<'comments'>, etc. throughout your project with zero extra ceremony:
import type { CollectionId, Doc, UntypedRow } from './types'
// Typed function — knows exactly which fields exist on a post
function formatPost(doc: Doc<'posts'>): string {
return `${doc.title} by ${doc.authorId}` // ← autocomplete + type checking
}
// Generic function — works with any collection's documents
function stripMeta(row: UntypedRow): Record<string, unknown> {
const result: Record<string, unknown> = {}
for (const [key, value] of Object.entries(row)) {
if (!key.startsWith('$')) result[key] = value
}
return result
}Distinguishing Singletons from Multi-Row Collections
If your schema has both single-document collections (e.g., user settings, app config) and multi-row collections (e.g., posts, comments), you can encode that distinction:
type SingletonIds = 'user_settings' | 'app_config'
/** Pulled data shape — singletons are a single doc or undefined, arrays for the rest. */
export type PulledCollections = {
[K in CollectionId]: K extends SingletonIds ? Doc<K> | undefined : Doc<K>[]
}This gives you a single type representing an entire pull from Appwrite, with each collection correctly typed as either a document or an array of documents.
Notes
- The TSS plugin watches the directory containing
appwrite.config.json(not the file itself) so that atomic writes like those fromappwrite pull(write tmp → rename) are detected reliably. - The plugin uses
export = init(CommonJS) as required by the TypeScript Server Plugin API. Your root tsconfig should exclude the plugin's source directory (e.g.packages/*/src) to avoid module format conflicts. - Peer dependency:
typescript ^5. - The CLI uses Commander for argument parsing.
Development
bun run build # Compile with tsc
bun run dev # Watch mode
bun test # Run tests
bun run typecheck # Type-check without emitting
bun run clean # Remove dist/