{"id":15715750,"url":"https://github.com/hacksur/model-one","last_synced_at":"2025-05-12T21:50:19.165Z","repository":{"id":55340378,"uuid":"521416801","full_name":"hacksur/model-one","owner":"hacksur","description":"Set of utility classes for Cloudflare D1","archived":false,"fork":false,"pushed_at":"2025-05-06T20:30:39.000Z","size":965,"stargazers_count":14,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-06T21:33:48.454Z","etag":null,"topics":["cloudflare","cloudflare-workers","crud","d1","joi-validation","model","raw-sql","schema","sqlite","validation","workers"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hacksur.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-08-04T21:11:04.000Z","updated_at":"2025-05-06T20:29:56.000Z","dependencies_parsed_at":"2024-10-24T12:58:17.360Z","dependency_job_id":"7df6b96a-842f-4a0a-943f-5bde4551ea1a","html_url":"https://github.com/hacksur/model-one","commit_stats":{"total_commits":20,"total_committers":2,"mean_commits":10.0,"dds":0.09999999999999998,"last_synced_commit":"f524ad8431a0e4b61ffb1e58a478e12b40ef33f5"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacksur%2Fmodel-one","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacksur%2Fmodel-one/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacksur%2Fmodel-one/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacksur%2Fmodel-one/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hacksur","download_url":"https://codeload.github.com/hacksur/model-one/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252775680,"owners_count":21802453,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-workers","crud","d1","joi-validation","model","raw-sql","schema","sqlite","validation","workers"],"created_at":"2024-10-03T21:42:43.884Z","updated_at":"2025-05-12T21:50:19.154Z","avatar_url":"https://github.com/hacksur.png","language":"TypeScript","readme":"# Model One\n\n[![code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)\n[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)\n[![made with lass](https://img.shields.io/badge/made_with-lass-95CC28.svg)](https://lass.js.org)\n[![license](https://img.shields.io/github/license/hacksur/model-one.svg)](LICENSE)\n[![npm downloads](https://img.shields.io/npm/dt/model-one.svg)](https://npm.im/model-one)\n\nA powerful ORM-like library for Cloudflare Workers D1 with validation support via Joi, inspired by [reform](https://github.com/trailblazer/reform).\n\n## Features\n\n- **Type-safe models** with TypeScript support\n- **Basic CRUD operations** with a PostgreSQL-like interface\n- **Enhanced column types** including string, number, boolean, date, and JSON\n- **UUID generation** by default for primary keys\n- **Automatic timestamps** for created_at and updated_at fields\n- **Soft delete functionality** for non-destructive record removal\n- **Data serialization and deserialization** for complex data types\n- **Form validation** powered by Joi\n- **Raw SQL query support** for complex operations\n- **Proper data encapsulation** through the data property pattern\n\n## Table of Contents\n\n1. [Installation](#installation)\n2. [Quick Start](#quick-start)\n3. [Model Definition](#model-definition)\n4. [Schema Configuration](#schema-configuration)\n5. [Column Types and Constraints](#column-types-and-constraints)\n6. [Form Validation](#form-validation)\n7. [CRUD Operations](#crud-operations)\n8. [Soft Delete](#soft-delete)\n9. [Extending Models](#extending-models)\n10. [TypeScript Support](#typescript-support)\n\n## Installation\n\n[npm][]:\n\n```sh\nnpm install model-one joi\n```\n\n[yarn][]:\n\n```sh\nyarn add model-one joi\n```\n\n## Quick Start\n\n```typescript\nimport { Model, Schema, Form } from 'model-one';\nimport Joi from 'joi';\n\n// Define schema\nconst userSchema = new Schema({\n  table_name: 'users',\n  columns: [\n    { name: 'id', type: 'string' },\n    { name: 'name', type: 'string' },\n    { name: 'email', type: 'string' },\n    { name: 'preferences', type: 'jsonb' }\n  ],\n  timestamps: true,\n  softDeletes: true\n});\n\n// Define validation schema\nconst joiSchema = Joi.object({\n  id: Joi.string(),\n  name: Joi.string().required(),\n  email: Joi.string().email().required(),\n  preferences: Joi.object()\n});\n\n// Define interfaces\ninterface UserDataI {\n  id?: string;\n  name?: string;\n  email?: string;\n  preferences?: Record\u003cstring, any\u003e;\n}\n\ninterface UserI extends Model {\n  data: UserDataI;\n}\n\n// Create form class\nclass UserForm extends Form {\n  constructor(data: UserI) {\n    super(joiSchema, data);\n  }\n}\n\n// Create model class\nclass User extends Model implements UserI {\n  data: UserDataI;\n\n  constructor(props: UserDataI = {}) {\n    super(userSchema);\n    this.data = props || {};\n  }\n}\n\n// Usage example\nasync function createUser(env) {\n  const userData = { name: 'John Doe', email: 'john@example.com', preferences: { theme: 'dark' } };\n  const user = new User(userData);\n  const form = new UserForm(user);\n  \n  const createdUser = await User.create(form, env.DB);\n  console.log(createdUser.data.id); // Auto-generated UUID\n  console.log(createdUser.data.name); // 'John Doe'\n  console.log(createdUser.data.preferences.theme); // 'dark'\n}\n```\n\n## Model Definition\n\nModels in Model-One follow a specific pattern to ensure type safety and proper data encapsulation:\n\n```typescript\n// Define your data interface\ninterface EntityDataI {\n  id?: string;\n  // Add your custom properties here\n  name?: string;\n  // etc...\n}\n\n// Define your model interface that extends the base Model\ninterface EntityI extends Model {\n  data: EntityDataI;\n}\n\n// Create your model class\nclass Entity extends Model implements EntityI {\n  data: EntityDataI;\n\n  constructor(props: EntityDataI = {}) {\n    super(entitySchema);\n    this.data = props || {};\n  }\n}\n```\n\n### Important Note on Data Access\n\nIn Model-One v0.2.0 and above, all entity properties must be accessed through the `data` property:\n\n```typescript\n// Correct way to access properties\nconst user = await User.findById(id, env.DB);\nif (user) {\n  console.log(user.data.name); // ✅ Correct\n  console.log(user.data.email); // ✅ Correct\n}\n\n// Incorrect way (will not work)\nconsole.log(user.name); // ❌ Incorrect\nconsole.log(user.email); // ❌ Incorrect\n```\n\n```sh\nyarn add model-one joi\n```\n\n## Schema Configuration\n\nThe Schema class is used to define your database table structure:\n\n```typescript\nconst entitySchema = new Schema({\n  table_name: 'entities',  // Name of the database table\n  columns: [\n    { name: 'id', type: 'string' },  // Primary key (UUID by default)\n    { name: 'title', type: 'string' },\n    { name: 'count', type: 'number' },\n    { name: 'is_active', type: 'boolean' },\n    { name: 'metadata', type: 'jsonb' },\n    { name: 'published_at', type: 'date' }\n  ],\n  timestamps: true,  // Adds created_at and updated_at columns\n  softDeletes: true  // Adds deleted_at column for soft deletes\n});\n```\n\n## Column Types and Constraints\n\nModel-One supports the following column types:\n\n| Type | JavaScript Type | Description |\n|------|----------------|-------------|\n| `string` | `string` | Text data |\n| `number` | `number` | Numeric data |\n| `boolean` | `boolean` | Boolean values (true/false) |\n| `date` | `Date` | Date and time values |\n| `jsonb` | `object` or `array` | JSON data that is automatically serialized/deserialized |\n\n## Form Validation\n\nModel-One uses Joi for form validation:\n\n```typescript\nimport Joi from 'joi';\nimport { Form } from 'model-one';\n\n// Define validation schema\nconst joiSchema = Joi.object({\n  id: Joi.string(),\n  title: Joi.string().required().min(3).max(100),\n  count: Joi.number().integer().min(0),\n  is_active: Joi.boolean(),\n  metadata: Joi.object(),\n  published_at: Joi.date()\n});\n\n// Create form class\nclass EntityForm extends Form {\n  constructor(data: EntityI) {\n    super(joiSchema, data);\n  }\n}\n\n// Usage\nconst entity = new Entity({ title: 'Test' });\nconst form = new EntityForm(entity);\n\n// Validation happens automatically when creating or updating\nconst createdEntity = await Entity.create(form, env.DB);\n```\n\n## CRUD Operations\n\nModel-One provides the following CRUD operations:\n\n### Create\n\n```typescript\n// Create a new entity\nconst entity = new Entity({ title: 'New Entity', count: 42 });\nconst form = new EntityForm(entity);\nconst createdEntity = await Entity.create(form, env.DB);\n\n// Access the created entity's data\nconsole.log(createdEntity.data.id); // Auto-generated UUID\nconsole.log(createdEntity.data.title); // 'New Entity'\n```\n\n### Read\n\n```typescript\n// Find by ID\nconst entity = await Entity.findById('some-uuid', env.DB);\nif (entity) {\n  console.log(entity.data.title);\n}\n\n// Find by column value\nconst entity = await Entity.findOne('title', 'New Entity', env.DB);\nif (entity) {\n  console.log(entity.data.count);\n}\n\n// Get all entities\nconst allEntities = await Entity.all(env.DB);\nallEntities.forEach(entity =\u003e {\n  console.log(entity.data.title);\n});\n```\n\n### Update\n\n```typescript\n// Update an entity\nconst updatedData = {\n  id: 'existing-uuid',  // Required for updates\n  title: 'Updated Title',\n  count: 100\n};\nconst updatedEntity = await Entity.update(updatedData, env.DB);\n\n// Access the updated entity's data\nconsole.log(updatedEntity.data.title); // 'Updated Title'\nconsole.log(updatedEntity.data.updated_at); // Current timestamp\n```\n\n### Delete (Soft Delete)\n\n```typescript\n// Soft delete an entity\nawait Entity.delete('entity-uuid', env.DB);\n\n// Entity will no longer be returned in queries\nconst notFound = await Entity.findById('entity-uuid', env.DB);\nconsole.log(notFound); // null\n```\n\n## Raw SQL Queries\n\nFor more complex operations, you can use raw SQL queries:\n\n```typescript\n// Execute a raw SQL query\nconst { results } = await Entity.raw(\n  'SELECT * FROM entities WHERE count \u003e 50 ORDER BY created_at DESC LIMIT 10',\n  env.DB\n);\n\nconsole.log(results); // Array of raw database results\n```\n\n## TypeScript Support\n\nModel-One is built with TypeScript and provides full type safety. To get the most out of it, define proper interfaces for your models:\n\n```typescript\n// Define your data interface\ninterface EntityDataI {\n  id?: string;\n  title?: string;\n  count?: number;\n  is_active?: boolean;\n  metadata?: Record\u003cstring, any\u003e;\n  published_at?: Date;\n  created_at?: Date;\n  updated_at?: Date;\n}\n\n// Define your model interface\ninterface EntityI extends Model {\n  data: EntityDataI;\n}\n\n// Implement your model class\nclass Entity extends Model implements EntityI {\n  data: EntityDataI;\n\n  constructor(props: EntityDataI = {}) {\n    super(entitySchema);\n    this.data = props || {};\n  }\n}\n```\n\n## Breaking Changes in v0.2.0\n\n### Data Property Access\n\nIn v0.2.0, all entity properties must be accessed through the `data` property:\n\n```typescript\n// v0.1.x (no longer works)\nconst user = await User.findById(id, env.DB);\nconsole.log(user.name); // ❌ Undefined\n\n// v0.2.0 and above\nconst user = await User.findById(id, env.DB);\nconsole.log(user.data.name); // ✅ Works correctly\n```\n\n### Model Initialization\n\nModels now require proper initialization of the `data` property:\n\n```typescript\n// Correct initialization in v0.2.0\nclass User extends Model implements UserI {\n  data: UserDataI;\n\n  constructor(props: UserDataI = {}) {\n    super(userSchema);\n    this.data = props || {}; // Initialize with empty object if props is undefined\n  }\n}\n```\n\n1. Create a new database.\n\nCreate a local file schema.sql\n\n```sql\nDROP TABLE IF EXISTS users;\n\nCREATE TABLE users (\n  id text PRIMARY KEY,\n  first_name text,\n  last_name text,\n  deleted_at datetime,\n  created_at datetime,\n  updated_at datetime\n);\n```\nCreates a new D1 database and provides the binding and UUID that you will put in your wrangler.toml file. \n```sh\nnpx wrangler d1 create example-db\n```\n\nCreate the tables from schema.sql\n\n```sh\nnpx wrangler d1 execute example-db --file ./schema.sql\n```\n\n2. 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 \n\n\n```js\n// ./models/User.ts\nimport { Model, Schema } from 'model-one'\nimport type { SchemaConfigI, Column } from 'model-one';\n\nconst userSchema: SchemaConfigI = new Schema({\n  table_name: 'users',\n  columns: [\n    { name: 'id', type: 'string', constraints: [{ type: 'PRIMARY KEY' }] },\n    { name: 'first_name', type: 'string' },\n    { name: 'last_name', type: 'string' }\n  ],\n  timestamps: true, // Optional, defaults to true\n  softDeletes: false // Optional, defaults to false\n})\n\n```\n\n3. Then we are going to define the interfaces for our User model.\n\n```js\n// ./interfaces/index.ts\nexport interface UserDataI {\n  id?: string\n  first_name?: string\n  last_name?: string\n}\n\nexport interface UserI extends Model {\n  data: UserDataI\n}\n```\n\n4. Now we are going import the types and extend the User\n\n```js\n// ./models/User.ts\nimport { UserI, UserDataI } from '../interfaces'\n\nexport class User extends Model implements UserI {\n  data: UserDataI\n\n  constructor(props: UserDataI) {\n    super(userSchema, props)\n    this.data = props\n  }\n}\n\n```\n\n5. Final result of the User model\n\n```js\n// ./models/User.ts\nimport { Model, Schema } from 'model-one'\nimport type { SchemaConfigI, Column } from 'model-one';\nimport { UserI, UserDataI } from '../interfaces'\n\nconst userSchema: SchemaConfigI = new Schema({\n  table_name: 'users',\n  columns: [\n    { name: 'id', type: 'string', constraints: [{ type: 'PRIMARY KEY' }] },\n    { name: 'first_name', type: 'string' },\n    { name: 'last_name', type: 'string' }\n  ],\n  timestamps: true,\n  softDeletes: false\n})\n\nexport class User extends Model implements UserI {\n  data: UserDataI\n\n  constructor(props: UserDataI) {\n    super(userSchema, props)\n    this.data = props\n  }\n}\n\n```\n\n\n6. 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.\n\n```js\n// ./forms/UserForm.ts\nimport { Form } from 'model-one'\nimport { UserI } from '../interfaces'\nimport Joi from 'joi'\n\nconst schema = Joi.object({\n  id: Joi.string(),\n  first_name: Joi.string(),\n  last_name: Joi.string(),\n})\n\nexport class UserForm extends Form {\n  constructor(data: UserI) {\n    super(schema, data)\n  }\n}\n\n\n```\n\n## Column Types and Constraints\n\n### Column Types\n\nmodel-one supports the following column types that map to SQLite types:\n\n```typescript\n// JavaScript column types\ntype ColumnType = \n  | 'string'   // SQLite: TEXT\n  | 'number'   // SQLite: INTEGER or REAL\n  | 'boolean'  // SQLite: INTEGER (0/1)\n  | 'jsonb'    // SQLite: TEXT (JSON stringified)\n  | 'date';    // SQLite: TEXT (ISO format)\n\n// SQLite native types\ntype SQLiteType = \n  | 'TEXT' \n  | 'INTEGER' \n  | 'REAL' \n  | 'NUMERIC' \n  | 'BLOB' \n  | 'JSON' \n  | 'BOOLEAN' \n  | 'TIMESTAMP' \n  | 'DATE';\n```\n\nExample usage:\n\n```typescript\nconst columns = [\n  { name: 'id', type: 'string', sqliteType: 'TEXT' },\n  { name: 'name', type: 'string' },\n  { name: 'age', type: 'number', sqliteType: 'INTEGER' },\n  { name: 'active', type: 'boolean' },\n  { name: 'metadata', type: 'jsonb' },\n  { name: 'created', type: 'date' }\n];\n```\n\n### Column Constraints\n\nYou can add constraints to your columns:\n\n```typescript\ntype ConstraintType = \n  | 'PRIMARY KEY' \n  | 'NOT NULL' \n  | 'UNIQUE' \n  | 'CHECK' \n  | 'DEFAULT' \n  | 'FOREIGN KEY';\n\ninterface Constraint {\n  type: ConstraintType;\n  value?: string | number | boolean;\n}\n```\n\nExample:\n\n```typescript\nconst columns = [\n  { \n    name: 'id', \n    type: 'string', \n    constraints: [{ type: 'PRIMARY KEY' }] \n  },\n  { \n    name: 'email', \n    type: 'string', \n    constraints: [{ type: 'UNIQUE' }, { type: 'NOT NULL' }] \n  },\n  { \n    name: 'status', \n    type: 'string', \n    constraints: [{ type: 'DEFAULT', value: 'active' }] \n  }\n];\n```\n\n## Schema Configuration\n\nYou can configure your schema with additional options:\n\n```typescript\nconst schema = new Schema({\n  table_name: 'users',\n  columns: [...],\n  uniques: ['email', 'username'], // Composite unique constraints\n  timestamps: true,  // Adds created_at and updated_at columns (default: true)\n  softDeletes: true  // Enables soft delete functionality (default: false)\n});\n```\n\n## Methods\n\n### Create\n\nTo 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.\n\n```js\n// ./controllers/UserController.ts\nimport { UserForm } from '../form/UserForm';\nimport { User } from '../models/User';\n\nconst userForm = new UserForm(new User({ first_name, last_name }))\n\nawait User.create(userForm, binding)\n\n```\n\n### Read\n\nBy importing the User model will have the following methods to query to D1:\n\n```js\n// ./controllers/UserController.ts\nimport { User } from '../models/User';\n\nawait User.all(binding)\n\nawait User.findById(id, binding)\n\nawait User.findOne(column, value, binding)\n\nawait User.findBy(column, value, binding)\n\n```\n\n### Update\n\nInclude the ID and the fields you want to update inside the data object.\n\n```js\n// ./controllers/UserController.ts\nimport { User } from '../models/User';\n\n// User.update(data, binding)\nawait User.update({ id, first_name: 'John' }, binding)\n\n```\n\n### Delete\n\nDelete a User\n\n```js\n// ./controllers/UserController.ts\n\nimport { User } from '../models/User';\n\nawait User.delete(id, binding)\n\n```\n\n### Raw SQL Queries\n\nExecute raw SQL queries with the new raw method:\n\n```js\n// ./controllers/UserController.ts\nimport { User } from '../models/User';\n\nconst { success, results } = await User.raw(\n  `SELECT * FROM users WHERE first_name LIKE '%John%'`, \n  binding\n);\n\nif (success) {\n  console.log(results);\n}\n```\n\n## Soft Delete\n\nWhen enabled in your schema configuration, soft delete will set the `deleted_at` timestamp instead of removing the record:\n\n```typescript\nconst userSchema = new Schema({\n  table_name: 'users',\n  columns: [...],\n  softDeletes: true // Enable soft delete\n});\n```\n\nWhen soft delete is enabled:\n- `delete()` will update the `deleted_at` field instead of removing the record\n- `all()`, `findById()`, `findOne()`, and `findBy()` will automatically filter out soft-deleted records\n- You can still access soft-deleted records with raw SQL queries if needed\n\n## Extend Methods\n\nExtend User methods.\n\n```js\n// ./models/User.ts\nimport { Model, Schema, NotFoundError } from 'model-one'\nimport type { SchemaConfigI } from 'model-one';\nimport { UserI, UserDataI } from '../interfaces'\n\nconst userSchema: SchemaConfigI = new Schema({\n  table_name: 'users',\n  columns: [\n    { name: 'id', type: 'string' },\n    { name: 'first_name', type: 'string' },\n    { name: 'last_name', type: 'string' }\n  ],\n})\n\nexport class User extends Model implements UserI {\n  data: UserDataI\n\n  constructor(props: UserDataI) {\n    super(userSchema, props)\n    this.data = props\n  }\n\n  static async findByFirstName(first_name: string, binding: any) {\n    // this.findBy(column, value, binding)\n    return await this.findBy('first_name', first_name, binding)\n  }\n\n  static async rawAll(binding: any) {\n    const { results, success } = await binding.prepare(`SELECT * FROM ${userSchema.table_name};`).all()\n    return Boolean(success) ? results : NotFoundError\n  }\n}\n\n```\n\n## To do:\n\n- [x] Support JSONB\n- [x] Enhanced column types and constraints\n- [x] Soft and hard delete\n- [x] Basic tests\n- [ ] Associations: belongs_to, has_one, has_many\n- [ ] Complex Forms for multiple Models\n\n## Contributors\nJulian Clatro\n\n## License\nMIT\n\n[npm]: https://www.npmjs.com/\n\n[yarn]: https://yarnpkg.com/\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacksur%2Fmodel-one","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhacksur%2Fmodel-one","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacksur%2Fmodel-one/lists"}