A lightweight, type-safe Google Sheets ORM for Node.js and Bun, inspired by Prisma.
Turn your Google Sheets into a database with a simple, familiar API.
- Type-safe Schema: Define your schema in TypeScript with support for
string,number,boolean,json,uuid,cuid, anddate. - Schema Inference: Automatically generate TypeScript interfaces from your schema definitions.
- Auto-generation: Built-in support for generating UUIDs, CUIDs, and Auto-incrementing numbers.
- Timestamps: Automatic
createdAt,updatedAt, and soft delete support withdeletedAt. - Relationships: Define
hasOne,hasMany, andbelongsTorelationships. - Eager Loading: Fetch related data easily with
include. - Advanced Querying: Support for filtering,
select,limit,skip,sortBy, andsortOrder. - Formula Support: Pass Google Sheets formulas to any field for calculated columns.
- Cross-Platform: Works seamlessly in Node.js and Bun.
npm install tarang-db
# or
bun add tarang-db- Google Cloud Project: Create a project in the Google Cloud Console.
- Enable Sheets API: Enable the Google Sheets API for your project.
- Service Account: Create a service account and download the JSON key file.
- Share Sheet: Share your Google Sheet with the service account email address (e.g.,
tarang-db@your-project.iam.gserviceaccount.com) with Editor access.
import { TarangClient } from 'tarang-db';
const client = new TarangClient({
spreadsheetId: 'YOUR_SPREADSHEET_ID',
auth: {
clientEmail: 'YOUR_SERVICE_ACCOUNT_EMAIL',
privateKey: 'YOUR_PRIVATE_KEY', // from service account JSON
},
cacheTTL: 60000, // Optional: Cache read results for 60 seconds (default)
maxCacheSize: 100, // Optional: Max number of entries in cache (default 100)
});TarangDB includes a built-in in-memory cache to reduce Google Sheets API quota usage.
- Read Operations:
findMany,findFirst, and internal lookups are cached. - Write Operations:
create,update,deleteautomatically invalidate the cache for the specific sheet being modified. - Configuration:
cacheTTL: Time to live in milliseconds (default: 60000). Set to0to disable.maxCacheSize: Maximum number of cache entries (default: 100). Oldest entries are evicted when limit is reached.
import { Model, Schema, DataTypes, Infer } from 'tarang-db';
// Define Schema
const UserSchema = new Schema({
id: { type: DataTypes.UUID, unique: true }, // Auto-generated UUID
name: DataTypes.String, // Shorthand
email: { type: DataTypes.String, unique: true },
age: DataTypes.Number, // Shorthand
birthDate: DataTypes.Date, // Plain Date field
isActive: { type: DataTypes.Boolean, default: true },
metadata: DataTypes.JSON,
createdAt: DataTypes.Date.createdAt(),
updatedAt: DataTypes.Date.updatedAt(),
deletedAt: DataTypes.Date.deletedAt(), // Enables soft delete
});
// Infer TypeScript Interface
type User = Infer<typeof UserSchema>;
// Initialize Model
const userModel = new Model<User>(client, {
sheetName: 'Users', // Name of the tab in Google Sheets
schema: UserSchema,
});TarangDB uses a schema definition object where keys are column names and values are column definitions.
| Type | Description |
|---|---|
DataTypes.String |
Text string |
DataTypes.Number |
Numeric value |
DataTypes.Boolean |
Boolean value (true/false) |
DataTypes.Date |
Date object (stored as ISO string) |
DataTypes.JSON |
JSON object (stored as stringified JSON) |
DataTypes.UUID |
UUID v4 string |
DataTypes.CUID |
CUID string |
| Modifier | Description |
|---|---|
unique |
Ensures values in the column are unique. |
default |
Sets a default value if none is provided. |
autoIncrement |
(Number only) Auto-increments the value. |
createdAt() |
(Date only) Sets current date on creation. |
updatedAt() |
(Date only) Updates date on modification. |
deletedAt() |
(Date only) Used for soft deletes. |
const user = await userModel.create({
name: 'Alice',
email: 'alice@example.com',
age: 25,
});Batch create multiple records.
const users = await userModel.createMany([
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Charlie', email: 'charlie@example.com' },
]);// Find all
const allUsers = await userModel.findMany();
// Filter
const adults = await userModel.findMany({ age: { gte: 18 } });
// Pagination & Sorting
const pagedUsers = await userModel.findMany(
{ isActive: true },
{
limit: 10,
skip: 0,
sortBy: 'createdAt',
sortOrder: 'desc'
}
);
// Select specific fields
const namesOnly = await userModel.findMany({}, { select: { name: true } });const user = await userModel.findFirst({ email: 'alice@example.com' });// Update by filter
const updated = await userModel.update(
{ email: 'alice@example.com' },
{ age: 26 }
);Create if not exists, otherwise update.
const user = await userModel.upsert({
where: { email: 'alice@example.com' },
update: { age: 26 },
create: {
name: 'Alice',
email: 'alice@example.com',
age: 26
},
});// Soft delete (if deletedAt is defined in schema)
await userModel.delete({ email: 'alice@example.com' });
// Hard delete (permanently remove row)
await userModel.delete({ email: 'alice@example.com' }, { force: true });TarangDB supports the following operators:
gt: Greater thanlt: Less thangte: Greater than or equallte: Less than or equalne: Not equallike: String matching (case-sensitive, supports%and_)ilike: String matching (case-insensitive, supports%and_)
// Users between 20 and 30
const users = await userModel.findMany({
age: { gte: 20, lte: 30 }
});
// Users starting with 'A'
const aUsers = await userModel.findMany({
name: { like: 'A%' }
});
// Users containing 'john' (case-insensitive)
const johns = await userModel.findMany({
name: { ilike: '%john%' }
});Define relationships in the Model configuration.
- hasOne: One-to-one relationship.
- hasMany: One-to-many relationship.
- belongsTo: Inverse of hasOne or hasMany.
// ... Schema definitions for User and Post ...
const userModel = new Model<User>(client, {
sheetName: 'Users',
schema: UserSchema,
relations: {
posts: {
type: 'hasMany',
targetModel: postModel,
foreignKey: 'userId',
localKey: 'id',
},
},
});
const postModel = new Model<Post>(client, {
sheetName: 'Posts',
schema: PostSchema,
relations: {
author: {
type: 'belongsTo',
targetModel: userModel,
foreignKey: 'userId',
localKey: 'id',
},
},
});
// Query with relations
const userWithPosts = await userModel.findFirst(
{ email: 'alice@example.com' },
{
include: {
posts: true,
// Nested include with select
profile: {
select: { bio: true }
}
}
}
);Batch create multiple records efficiently.
const users = await userModel.createMany([
{ name: 'Bob', email: 'bob@example.com' },
{ name: 'Charlie', email: 'charlie@example.com' },
]);Create a record if it doesn't exist, or update it if it does.
const user = await userModel.upsert({
where: { email: 'alice@example.com' },
update: { age: 27 },
create: {
name: 'Alice',
email: 'alice@example.com',
age: 26
},
});If your schema includes a deletedAt field using DataTypes.Date.deletedAt(), the delete method will perform a soft delete by default.
// Soft delete (sets deletedAt timestamp)
await userModel.delete({ email: 'alice@example.com' });
// Hard delete (permanently removes the row)
await userModel.delete({ email: 'alice@example.com' }, { force: true });
// Include soft-deleted records in queries
const allUsersIncludingDeleted = await userModel.findMany(
{},
{ includeDeleted: true }
);You can pass Google Sheets formulas to any field. This is useful for calculated columns.
await productModel.create({
name: 'iPhone',
price: 30000,
qty: 2,
// Formula to calculate total: price * qty
total: '=INDIRECT("R[0]C[-2]", FALSE) * INDIRECT("R[0]C[-1]", FALSE)'
});MIT