npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

model-one

v0.4.1

Published

Model for D1

Readme

Model One

code style styled with prettier made with lass license npm downloads

A powerful TypeScript ORM library (v0.4.0) for Cloudflare Workers D1 with type-safe operations, automatic validation, and schema-driven development.

🚀 What's New in v0.4.0

  • ✅ Full Cloudflare D1 Compatibility: Fixed SQL parameter binding for production-ready D1 support
  • ✅ Enhanced Boolean Type Handling: Robust conversion between SQLite 0/1 and JavaScript true/false
  • ✅ Advanced Error Reporting: Comprehensive error context and debugging information
  • ✅ Joi Schema Auto-Generation: Automatic validation schemas that align with database types
  • ✅ TypeScript Interface Generation: Auto-generated interfaces from schema definitions
  • ✅ Enhanced Form Validation: Input/output validation modes with detailed error reporting

Features

  • Type-safe models with TypeScript support
  • Basic CRUD operations with a PostgreSQL-like interface
  • Enhanced column types including string, number, boolean, date, and JSON
  • UUID generation by default for primary keys
  • Automatic timestamps for created_at and updated_at fields
  • Soft delete functionality for non-destructive record removal
  • Data serialization and deserialization for complex data types
  • Form validation powered by Joi
  • Raw SQL query support for complex operations
  • Proper data encapsulation through the data property pattern

Table of Contents

  1. Installation
  2. Quick Start
  3. Model Definition
  4. Schema Configuration
  5. Column Types and Constraints
  6. Form Validation
  7. CRUD Operations
  8. Soft Delete
  9. Extending Models
  10. TypeScript Support

Installation

npm:

npm install model-one joi

yarn:

yarn add model-one joi

Quick Start

import { Model, Schema, Form } from 'model-one';
import Joi from 'joi';

// Define schema
const userSchema = new Schema({
  table_name: 'users',
  columns: [
    { name: 'id', type: 'string' },
    { name: 'name', type: 'string' },
    { name: 'email', type: 'string' },
    { name: 'preferences', type: 'jsonb' }
  ],
  timestamps: true,
  softDeletes: true
});

// Define validation schema
const joiSchema = Joi.object({
  id: Joi.string(),
  name: Joi.string().required(),
  email: Joi.string().email().required(),
  preferences: Joi.object()
});

// Define interfaces
interface UserDataI {
  id?: string;
  name?: string;
  email?: string;
  preferences?: Record<string, any>;
}

interface UserI extends Model {
  data: UserDataI;
}

// Create form class
class UserForm extends Form {
  constructor(data: UserI) {
    super(joiSchema, data);
  }
}

// Create model class
class User extends Model implements UserI {
  data: UserDataI;

  constructor(props: UserDataI = {}) {
    super(userSchema);
    this.data = props || {};
  }
}

// Usage example
async function createUser(env) {
  const userData = { name: 'John Doe', email: '[email protected]', preferences: { theme: 'dark' } };
  const user = new User(userData);
  const form = new UserForm(user);
  
  const createdUser = await User.create(form, env.DB);
  console.log(createdUser.data.id); // Auto-generated UUID
  console.log(createdUser.data.name); // 'John Doe'
  console.log(createdUser.data.preferences.theme); // 'dark'
}

Model Definition

Models in Model-One follow a specific pattern to ensure type safety and proper data encapsulation:

// Define your data interface
interface EntityDataI {
  id?: string;
  // Add your custom properties here
  name?: string;
  // etc...
}

// Define your model interface that extends the base Model
interface EntityI extends Model {
  data: EntityDataI;
}

// Create your model class
class Entity extends Model implements EntityI {
  data: EntityDataI;

  constructor(props: EntityDataI = {}) {
    super(entitySchema);
    this.data = props || {};
  }
}

Important Note on Data Access

In Model-One v0.2.0 and above, all entity properties must be accessed through the data property:

// Correct way to access properties
const user = await User.findById(id, env.DB);
if (user) {
  console.log(user.data.name); // Correct
  console.log(user.data.email); // Correct
}

// Incorrect way (will not work)
console.log(user.name); // Incorrect
console.log(user.email); // Incorrect
yarn add model-one joi

Schema Configuration

The Schema class is used to define your database table structure:

const entitySchema = new Schema({
  table_name: 'entities',  // Name of the database table
  columns: [
    { name: 'id', type: 'string' },  // Primary key (UUID by default)
    { name: 'title', type: 'string' },
    { name: 'count', type: 'number' },
    { name: 'is_active', type: 'boolean' },
    { name: 'metadata', type: 'jsonb' },
    { name: 'published_at', type: 'date' }
  ],
  timestamps: true,  // Adds created_at and updated_at columns
  softDeletes: true  // Adds deleted_at column for soft deletes
});

Column Types and Constraints

Model-One supports the following column types:

| Type | JavaScript Type | Description | |------|----------------|-------------| | string | string | Text data | | number | number | Numeric data | | boolean | boolean | Boolean values (true/false) | | date | Date | Date and time values | | jsonb | object or array | JSON data that is automatically serialized/deserialized |

Form Validation

Model-One uses Joi for form validation:

import Joi from 'joi';
import { Form } from 'model-one';

// Define validation schema
const joiSchema = Joi.object({
  id: Joi.string(),
  title: Joi.string().required().min(3).max(100),
  count: Joi.number().integer().min(0),
  is_active: Joi.boolean(),
  metadata: Joi.object(),
  published_at: Joi.date()
});

// Create form class
class EntityForm extends Form {
  constructor(data: EntityI) {
    super(joiSchema, data);
  }
}

// Usage
const entity = new Entity({ title: 'Test' });
const form = new EntityForm(entity);

// Validation happens automatically when creating or updating
const createdEntity = await Entity.create(form, env.DB);

CRUD Operations

Model-One provides the following CRUD operations:

Create

// Create a new entity
const entity = new Entity({ title: 'New Entity', count: 42 });
const form = new EntityForm(entity);
const createdEntity = await Entity.create(form, env.DB);

// Access the created entity's data
console.log(createdEntity.data.id); // Auto-generated UUID
console.log(createdEntity.data.title); // 'New Entity'

Read (Finding Records)

Model-One provides several static methods on your model class to retrieve records from the database. All these methods return model instances (or null / an array of instances), and you should access their properties via the .data object.

  • YourModel.findById(id: string, env: any, includeDeleted?: boolean): Promise<YourModel | null>

    Finds a single record by its ID. Returns a model instance or null if not found.

    const user = await User.findById('some-uuid', env.DB);
    if (user) {
      console.log(user.data.name); // Access data via .data
    }

    If softDeletes is enabled for the model, you can pass true as the third argument (includeDeleted) to also find soft-deleted records.

  • YourModel.findOne(column: string, value: string, env: any, includeDeleted?: boolean): Promise<YourModel | null>

    Finds the first record that matches a given column-value pair. Returns a model instance or null.

    const adminUser = await User.findOne('email', '[email protected]', env.DB);
    if (adminUser) {
      console.log(adminUser.data.id);
    }

    The optional fourth argument includeDeleted works the same as in findById.

  • YourModel.findBy(column: string, value: string, env: any, includeDeleted?: boolean): Promise<YourModel[]>

    Finds all records that match a given column-value pair. Returns an array of model instances (can be empty).

    const activeUsers = await User.findBy('status', 'active', env.DB);
    activeUsers.forEach(user => {
      console.log(user.data.email);
    });

    The optional fourth argument includeDeleted works the same as in findById.

  • YourModel.all(env: any, includeDeleted?: boolean): Promise<YourModel[]>

    Retrieves all records for the model. Returns an array of model instances.

    const allUsers = await User.all(env.DB);
    console.log(`Total users: ${allUsers.length}`);
    allUsers.forEach(user => {
      console.log(user.data.name); // Access data via .data
    });

    The optional second argument includeDeleted works the same as in findById.

Update

// Update an entity
const updatedData = {
  id: 'existing-uuid',  // Required for updates
  title: 'Updated Title',
  count: 100
};
const updatedEntity = await Entity.update(updatedData, env.DB);

// Access the updated entity's data
console.log(updatedEntity.data.title); // 'Updated Title'
console.log(updatedEntity.data.updated_at); // Current timestamp

Delete (Soft Delete)

// Soft delete an entity using the static Model.delete() method (still supported)
await Entity.delete('entity-uuid', env.DB);

// Entity will no longer be returned in queries by default
const notFound = await Entity.findById('entity-uuid', env.DB);
console.log(notFound); // null

// New: Soft delete an entity using the instance delete() method
const entityToDelete = await Entity.findById('another-entity-uuid', env.DB);
if (entityToDelete) {
  await entityToDelete.delete(env.DB);
  console.log('Entity soft deleted via instance method.');
}

Raw SQL Queries

For more complex operations, you can use raw SQL queries:

// Execute a raw SQL query
const { results } = await Entity.raw(
  'SELECT * FROM entities WHERE count > 50 ORDER BY created_at DESC LIMIT 10',
  env.DB
);

console.log(results); // Array of raw database results

TypeScript Support

Model-One is built with TypeScript and provides full type safety. To get the most out of it, define proper interfaces for your models:

// Define your data interface
interface EntityDataI {
  id?: string;
  title?: string;
  count?: number;
  is_active?: boolean;
  metadata?: Record<string, any>;
  published_at?: Date;
  created_at?: Date;
  updated_at?: Date;
}

// Define your model interface
interface EntityI extends Model {
  data: EntityDataI;
}

// Implement your model class
class Entity extends Model implements EntityI {
  data: EntityDataI;

  constructor(props: EntityDataI = {}) {
    super(entitySchema);
    this.data = props || {};
  }
}

Breaking Changes in v0.2.0

Data Property Access

In v0.2.0, all entity properties must be accessed through the data property:

// v0.1.x (no longer works)
const user = await User.findById(id, env.DB);
console.log(user.name); // Undefined

// v0.2.0 and above
const user = await User.findById(id, env.DB);
console.log(user.data.name); // Works correctly

Model Initialization

Models now require proper initialization of the data property:

// Correct initialization in v0.2.0
class User extends Model implements UserI {
  data: UserDataI;

  constructor(props: UserDataI = {}) {
    super(userSchema);
    this.data = props || {}; // Initialize with empty object if props is undefined
  }
}
  1. Create a new database.

Create a local file schema.sql

DROP TABLE IF EXISTS users;

CREATE TABLE users (
  id text PRIMARY KEY,
  first_name text,
  last_name text,
  deleted_at datetime,
  created_at datetime,
  updated_at datetime
);

Creates a new D1 database and provides the binding and UUID that you will put in your wrangler.toml file.

npx wrangler d1 create example-db

Create the tables from schema.sql

npx wrangler d1 execute example-db --file ./schema.sql
  1. We need to import the Model and Schema from 'model-one' and the type SchemaConfigI. Then create a new Schema, define table name and fields
// ./models/User.ts
import { Model, Schema } from 'model-one'
import type { SchemaConfigI, Column } from 'model-one';

const userSchema: SchemaConfigI = new Schema({
  table_name: 'users',
  columns: [
    { name: 'id', type: 'string', constraints: [{ type: 'PRIMARY KEY' }] },
    { name: 'first_name', type: 'string' },
    { name: 'last_name', type: 'string' }
  ],
  timestamps: true, // Optional, defaults to true
  softDeletes: false // Optional, defaults to false
})
  1. Then we are going to define the interfaces for our User model.
// ./interfaces/index.ts
export interface UserDataI {
  id?: string
  first_name?: string
  last_name?: string
}

export interface UserI extends Model {
  data: UserDataI
}
  1. Now we are going import the types and extend the User
// ./models/User.ts
import { UserI, UserDataI } from '../interfaces'

export class User extends Model implements UserI {
  data: UserDataI

  constructor(props: UserDataI) {
    super(userSchema, props)
    this.data = props
  }
}
  1. Final result of the User model
// ./models/User.ts
import { Model, Schema } from 'model-one'
import type { SchemaConfigI } from 'model-one';
import { UserI, UserDataI } from '../interfaces'

const userSchema: SchemaConfigI = new Schema({
  table_name: 'users',
  columns: [
    { name: 'id', type: 'string' },
    { name: 'first_name', type: 'string' },
    { name: 'last_name', type: 'string' }
  ],
})

export class User extends Model implements UserI {
  data: UserDataI

  constructor(props: UserDataI) {
    super(userSchema, props)
    this.data = props
  }
}
  1. After creating the User we are going to create the form that handles the validations. And with the help of Joi we are going to define the fields.
// ./forms/UserForm.ts
import { Form } from 'model-one'
import { UserI } from '../interfaces'
import Joi from 'joi'

const schema = Joi.object({
  id: Joi.string(),
  first_name: Joi.string(),
  last_name: Joi.string(),
})

export class UserForm extends Form {
  constructor(data: UserI) {
    super(schema, data)
  }
}

Column Types and Constraints

Column Types

model-one supports the following column types that map to SQLite types:

// JavaScript column types
type ColumnType = 
  | 'string'   // SQLite: TEXT
  | 'number'   // SQLite: INTEGER or REAL
  | 'boolean'  // SQLite: INTEGER (0/1)
  | 'jsonb'    // SQLite: TEXT (JSON stringified)
  | 'date';    // SQLite: TEXT (ISO format)

// SQLite native types
type SQLiteType = 
  | 'TEXT' 
  | 'INTEGER' 
  | 'REAL' 
  | 'NUMERIC' 
  | 'BLOB' 
  | 'JSON' 
  | 'BOOLEAN' 
  | 'TIMESTAMP' 
  | 'DATE';

Example usage:

const columns = [
  { name: 'id', type: 'string', sqliteType: 'TEXT' },
  { name: 'name', type: 'string' },
  { name: 'age', type: 'number', sqliteType: 'INTEGER' },
  { name: 'active', type: 'boolean' },
  { name: 'metadata', type: 'jsonb' },
  { name: 'created', type: 'date' }
];

Column Constraints

You can add constraints to your columns:

type ConstraintType = 
  | 'PRIMARY KEY' 
  | 'NOT NULL' 
  | 'UNIQUE' 
  | 'CHECK' 
  | 'DEFAULT' 
  | 'FOREIGN KEY';

interface Constraint {
  type: ConstraintType;
  value?: string | number | boolean;
}

Example:

const columns = [
  { 
    name: 'id', 
    type: 'string', 
    constraints: [{ type: 'PRIMARY KEY' }] 
  },
  { 
    name: 'email', 
    type: 'string', 
    constraints: [{ type: 'UNIQUE' }, { type: 'NOT NULL' }] 
  },
  { 
    name: 'status', 
    type: 'string', 
    constraints: [{ type: 'DEFAULT', value: 'active' }] 
  }
];

Schema Configuration

You can configure your schema with additional options:

const schema = new Schema({
  table_name: 'users',
  columns: [...],
  uniques: ['email', 'username'], // Composite unique constraints
  timestamps: true,  // Adds created_at and updated_at columns (default: true)
  softDeletes: true  // Enables soft delete functionality (default: false)
});

Methods

Create

To insert data we need to import the UserForm and we are going start a new User and insert it inside the UserForm, then we can call the method create.

// ./controllers/UserController.ts
import { UserForm } from '../form/UserForm';
import { User } from '../models/User';

const userForm = new UserForm(new User({ first_name, last_name }))

await User.create(userForm, binding)

Read

By importing the User model will have the following methods to query to D1:

// ./controllers/UserController.ts
import { User } from '../models/User';

await User.all(binding)

await User.findById(id, binding)

await User.findOne(column, value, binding)

await User.findBy(column, value, binding)

Update

Include the ID and the fields you want to update inside the data object.

// ./controllers/UserController.ts

import { User } from '../models/User';

// User.update(data, binding)
await User.update({ id, first_name: 'John' }, binding)

Delete

Delete a User

// ./controllers/UserController.ts

import { User } from '../models/User';

await User.delete(id, binding)

Raw SQL Queries

Execute raw SQL queries with the new raw method:

// ./controllers/UserController.ts
import { User } from '../models/User';

const { success, results } = await User.raw(
  `SELECT * FROM users WHERE first_name LIKE '%John%'`, 
  binding
);

if (success) {
  console.log(results);
}

Soft Delete

When enabled in your schema configuration, soft delete will set the deleted_at timestamp instead of removing the record:

const userSchema = new Schema({
  table_name: 'users',
  columns: [...],
  softDeletes: true // Enable soft delete
});

When soft delete is enabled:

  • delete() will update the deleted_at field instead of removing the record
  • all(), findById(), findOne(), and findBy() will automatically filter out soft-deleted records
  • You can still access soft-deleted records with raw SQL queries if needed

Extend Methods

Extend User methods.

// ./models/User.ts
import { Model, Schema, NotFoundError } from 'model-one'
import type { SchemaConfigI } from 'model-one';
import { UserI, UserDataI } from '../interfaces'

const userSchema: SchemaConfigI = new Schema({
  table_name: 'users',
  columns: [
    { name: 'id', type: 'string' },
    { name: 'first_name', type: 'string' },
    { name: 'last_name', type: 'string' }
  ],
})

export class User extends Model implements UserI {
  data: UserDataI

  constructor(props: UserDataI) {
    super(userSchema, props)
    this.data = props
  }

  static async findByFirstName(first_name: string, binding: any) {
    // this.findBy(column, value, binding)
    return await this.findBy('first_name', first_name, binding)
  }

  static async rawAll(binding: any) {
    const { results, success } = await binding.prepare(`SELECT * FROM ${userSchema.table_name};`).all()
    return Boolean(success) ? results : NotFoundError
  }
}

To do:

  • [x] Support JSONB
  • [x] Enhanced column types and constraints
  • [x] Soft and hard delete
  • [x] Basic tests
  • [ ] Associations: belongs_to, has_one, has_many
  • [ ] Complex Forms for multiple Models

Contributors

Julian Clatro

License

MIT