Skip to content

Manual API Usage

This guide covers using open-sheets-orm directly — defining schemas, working with the adapter, and querying with the Database class.

Schema Definition

Schemas describe your data models using a fluent builder API.

ts
import { schema, field } from 'open-sheets-orm';

const mySchema = schema({
  User: {
    id: field.string().primaryKey(),
    email: field.string().unique(),
    name: field.string().optional(),
    age: field.number().optional().default(0),
  },
  Post: {
    id: field.string().primaryKey(),
    title: field.string(),
    body: field.string().optional(),
    published: field.boolean().default(false),
    createdAt: field.date().optional(),
    metadata: field.json().optional(),
  },
});

Field Types

FactoryTypeScript TypeCell Parsing
field.string()stringRaw string value
field.number()numberNumber(value)
field.boolean()booleantrue if value is "true", "TRUE", or "1"
field.date()Datenew Date(value)
field.json()unknownJSON.parse(value)

Field Modifiers

ModifierEffect
.primaryKey()Marks as the primary key. Auto-generates a UUID on create if omitted from input. Enforces uniqueness.
.unique()Enforces uniqueness across all rows on create and update.
.optional()Field is not required on create.
.default(value)Used when the field is omitted from input data.

Adapter (Low-Level)

The adapter provides direct spreadsheet operations without schema validation.

GoogleSheetsAdapter

ts
import { GoogleSheetsAdapter } from 'open-sheets-orm';

const adapter = new GoogleSheetsAdapter(
  credentials, // Auth.JWTInput — service account JSON
  'spreadsheet-id', // Target spreadsheet ID
);
await adapter.connect();

Default Sheet Name

The adapter constructor accepts an optional third argument — sheetName. This sets a default sheet used by all low-level adapter methods when no explicit sheet name is passed at call time.

When using the Database class, you do not set this parameter. The ORM resolves the target sheet automatically by using the model name as the sheet name. For example, db.create('User', data) always operates on the sheet called "User", regardless of any default set on the adapter.

The default sheet name is only useful when working with the adapter directly (without the Database class). For example, if you have a single sheet and want to avoid passing the name to every call:

ts
const adapter = new GoogleSheetsAdapter(
  credentials,
  'spreadsheet-id',
  'Sheet1',
);
await adapter.connect();

// These calls all target "Sheet1" without specifying the name
const rows = await adapter.readSheet();
await adapter.appendRow(['1', 'Alice']);
const headers = await adapter.getHeaders();

If no default is set (and no explicit name is passed to a method), the adapter defaults to the first sheet in the spreadsheet.

Every adapter method also accepts an optional sheetName argument that overrides the constructor default:

ts
// Targets "OtherSheet" even though the default is "Sheet1"
const rows = await adapter.readSheet('OtherSheet');

Row Operations

ts
// Read all rows (returns objects keyed by headers)
const rows = await adapter.readSheet<User>();

// Append a new row
await adapter.appendRow(['1', 'Alice', 'alice@example.com']);

// Update a row by index (0-based, first data row = index 0)
await adapter.updateRow(0, ['1', 'Alice Updated', 'alice@example.com']);

// Delete a row by index
await adapter.deleteRow(0);

Sheet Management

ts
// Get spreadsheet metadata
const info = await adapter.getSpreadsheet();
console.log(info.title, info.sheets);

// Create a new sheet
await adapter.createSheet('NewTab');

// Ensure a sheet exists (no-op if already present)
await adapter.ensureSheet('NewTab');

// Delete a sheet
await adapter.deleteSheet('NewTab');

// Read/write headers
const headers = await adapter.getHeaders('Sheet1');
await adapter.writeHeaders(['id', 'name', 'email'], 'Sheet1');

Disconnect

ts
await adapter.disconnect();

Database (Query Engine)

The Database class provides ORM-style queries with schema validation, type coercion, and uniqueness enforcement.

ts
import { Database } from 'open-sheets-orm';

const db = new Database(mySchema, adapter);

When using the Database class, the model name is used as the sheet name automatically. For example, db.findMany('User') reads from the sheet called "User", and db.create('Product', data) writes to the sheet called "Product". There is no need to configure sheet names separately — each model in your schema maps directly to a sheet tab in the spreadsheet. If the sheet does not exist when you first write to it, the Database class creates it automatically.

findMany

Retrieve all records with optional filtering and pagination.

ts
// All records
const users = await db.findMany('User');

// With filtering (all conditions must match)
const alices = await db.findMany('User', {
  where: { name: 'Alice' },
});

// With pagination
const page = await db.findMany('User', {
  where: { name: 'Alice' },
  skip: 10,
  limit: 5,
});

findUnique

Find a single record by field values. Throws RecordNotFoundError if no match is found.

ts
import { RecordNotFoundError } from 'open-sheets-orm';

try {
  const user = await db.findUnique('User', { id: '123' });
} catch (e) {
  if (e instanceof RecordNotFoundError) {
    console.log('User not found');
  }
}

create

Insert a new record. Primary keys are auto-generated (UUID) if omitted. Default values are applied for missing fields.

ts
// Auto-generate ID
const user = await db.create('User', {
  email: 'bob@example.com',
  name: 'Bob',
});
console.log(user.id); // auto-generated UUID

// Provide explicit ID
const user2 = await db.create('User', {
  id: 'custom-id',
  email: 'carol@example.com',
});

Throws UniqueConstraintError if a primaryKey or unique field value already exists.

update

Update a record matching the where clause. Throws RecordNotFoundError if no match is found.

ts
import { UniqueConstraintError } from 'open-sheets-orm';

try {
  const updated = await db.update('User', { id: '123' }, { name: 'New Name' });
} catch (e) {
  if (e instanceof UniqueConstraintError) {
    console.log('Unique constraint violated');
  }
}

delete

Delete a record matching the where clause. Throws RecordNotFoundError if no match is found.

ts
await db.delete('User', { id: '123' });

Error Handling

All domain errors extend SheetsError:

ErrorWhen
ConnectionErrorAdapter method called before connect()
SheetNotFoundErrorReferenced sheet does not exist
RowNotFoundErrorRow index is out of bounds
ModelNotFoundErrorModel name not in schema
RecordNotFoundErrorNo record matches the where clause
UniqueConstraintErrorDuplicate PK/unique field value
ValidationErrorRequired field missing or type mismatch
SchemaValidationErrorInvalid schema definition
ts
import {
  SheetsError,
  ConnectionError,
  ModelNotFoundError,
  UniqueConstraintError,
} from 'open-sheets-orm';

Released under the MIT License.