{"id":23383768,"url":"https://github.com/mdwheele/yorm","last_synced_at":"2025-08-09T00:16:53.416Z","repository":{"id":194692894,"uuid":"691343643","full_name":"mdwheele/yorm","owner":"mdwheele","description":"Yet Another ORM 🤷","archived":false,"fork":false,"pushed_at":"2025-07-20T02:29:59.000Z","size":123,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-20T04:43:54.696Z","etag":null,"topics":[],"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/mdwheele.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":"2023-09-14T01:51:53.000Z","updated_at":"2025-07-20T02:30:34.000Z","dependencies_parsed_at":"2023-09-14T18:14:19.871Z","dependency_job_id":"0fc343cd-ad93-4462-b58e-c4b06615a662","html_url":"https://github.com/mdwheele/yorm","commit_stats":null,"previous_names":["mdwheele/yorm"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/mdwheele/yorm","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdwheele%2Fyorm","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdwheele%2Fyorm/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdwheele%2Fyorm/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdwheele%2Fyorm/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mdwheele","download_url":"https://codeload.github.com/mdwheele/yorm/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mdwheele%2Fyorm/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":269509357,"owners_count":24428892,"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","status":"online","status_checked_at":"2025-08-08T02:00:09.200Z","response_time":72,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":[],"created_at":"2024-12-21T22:29:54.452Z","updated_at":"2025-08-09T00:16:53.378Z","avatar_url":"https://github.com/mdwheele.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Yet Another ORM 🤷\n\nYORM is a super-simple and light-weight ORM built on top of [Knex](https://knexjs.org) and inspired by Laravel's [Eloquent ORM](https://laravel.com/docs/master/eloquent).\n\n```bash\n$ npm install yorm.js\n```\n\n```typescript\n// CommonJS \nconst { Model } = require('yorm.js')\n\n// ESM\nimport { Model } from 'yorm.js'\n\n// Configure database\nModel.configure({\n  client: 'sqlite3',\n  connection: {\n    filename: './database.sqlite'\n  }\n})\n```\n\n## How's it work?\n\nImagine writing migrations like...\n\n```js\nfunction up(knex) {\n  return knex.schema.createTable('users', table =\u003e {\n    table.uuid('id').defaultTo(knex.fn.uuid()).primary()\n    table.string('email').unique()\n    table.string('name')\n    table.timestamps(false, true)\n  })\n}\n\nfunction down(knex) {\n  return knex.schema.dropTable('users')  \n}\n```\n\nThen... imagine writing a class like...\n\n```js\nclass User extends Model {\n  id\n  email\n  name\n  created_at\n  updated_at\n}\n```\n\n... and then, imagine you could just start CRUD-ing from the `users` table ... :unicorn:\n\n```js\n// Add a new user to the database!\nconst user = await User.create({\n  email: 'susan@example.com',\n  name: 'Susan Example',\n})\n\n// Make a change!\nuser.name = 'S. Example'\n\n// Persist the change!\nawait user.save()\n\n// Fetch by primary key!\nawait User.find(user.id)\n\n// Fetch by whatever you want!\nawait User.where('email', 'susan@example.com') \n\n// Delete that jank!\nawait user.delete()\n```\n\n## Stop the bad from happening\n\nWhat if folks start adding random properties here and there? HOW IS YORM GONNA HANDLE THAT?!\n\n\u003cimg align=\"right\" width=\"300\" src=\"https://media.giphy.com/media/14wTbNneogwjba/giphy.gif\" /\u003e\n\n```js\nconst user = await User.create({\n  email: 'susan@example.com',\n  name: 'Susan Example',\n\n  // This will throw an error...\n  badProperty: true\n})\n\n// ... as will this.\nuser.badPropertyAfterTheFact = true\n```\n\nBefore any instance of a model is returned, we seal the object to prevent addition (or removal) of properties from the object. Only properties explicitly declared on the model are allowed... for now.\n\n## I have a legacy code base and my table names look like klingon :anger:\n\nBy default, model table names are computed to be a pluralization of the model name:\n\n  - A class named `User` will map to a table named `users`\n  - `Comment` will map to `comments`\n  - `BirdOfPrey` will map to `birdofpreys`\n\nHowever, you can always override this in your own model by overriding the static `table` property:\n\n```js\nclass User extends Model {\n  id\n  username\n  email\n\n  static table = 'maj'\n}\n```\n\n## I want to use those fancy UUID / ULID things\n\nWell good freakin' news... YORM lets you do whatever you want... seriously. By default, we delegate to your DBMS of choice to do the right thing when it comes to auto-incrementing or database-generated UUIDs and things like that. However, there are times where you'll want to generate an ID before persistance to the database. For that, we have the `generateKey` method.\n\n```js\nclass Example extends Model {\n  id\n\n  static function generateKey() {\n    return 'foo'\n  }\n}\n```\n\nIdentifiers must be returned as strings. You have control over generation of identifiers. That means it's up to you to make sure they're unique!\n\nTo make things simpler, we have some out-of-the-box support for [UUID](https://github.com/uuidjs/uuid), [ULID](https://github.com/perry-mitchell/ulidx) (which are lexicographically sortable), and [nanoid](https://github.com/ai/nanoid). To use these, just return `uuid`, `ulid`, or `nanoid`, respectively. \n\n```js\nclass Example extends Model {\n  id\n\n  static keyType = 'uuid'\n}\n```\n\n## Transactional transactions transacting!\n\nUnder the hood, YORM models are just a set of utility functions on top of a Knex instance. We use Knex to implement transactions. Normally, this means that we would have to create a transaction context and pass that around to every model that needs to take part in the transaction. However, by storing the current transaction context on the base Model, all instances can automatically make use of the transaction when they are saved. \n\nWe do not support nested transactions because honestly... I don't want to implement a static stack of transaction. Also, I think that shit is confusing and don't need to do it myself! However, if you really need it, open up an issue and we can do it.\n\n```js\nconst result = await Model.transaction(async (trx) =\u003e {\n  const user = new User({ name: 'John', email: 'john@example.com' })\n  await user.save()\n\n  const profile = new Profile({ \n    user_id: user.getKey(), \n    bio: 'Software developer' \n  })\n  await profile.save()\n\n  return { user, profile }\n})\n```\n\n## Softest of soft deletes\n\nIt's somewhat common to support features for \"undo\"-ing deletes. This is usually accomplished by replacing `DELETE FROM {table} WHERE ...` statements with an `UPDATE SET deleted_at = NOW() WHERE ...` and then having every query function account for this field. If `deleted_at` is `NULL`, the record exists. Otherwise, you have the date and time that the record was deleted.\n\n```js\nclass SoftDelete extends Model {\n  id\n  deleted_at\n\n  public static softDeletes = true\n}\n\nconst model = await SoftDelete.create()\n\nawait model.delete() // UPDATE softdeletes SET deleted_at = NOW() WHERE id = 'foo'\n```\n\n**Restoring deleted models**\n\nIf you have an instance of a model that was _just deleted_, you can call its `.restore()` method and it will be restored.\n\nMore commonly, you'll be recovering a model that was deleted in the past where you _do not_ have an instance. In these cases, you can use the static version of the same method to restore ALL models matching specific criteria:\n\n```js\nconst model = await SoftDelete.create()\n\nawait model.delete()\n\nawait model.restore()\n```\n\n## Hiding model properties from JSON\n\nThere are scenarios where you need to have a property on a model that shouldn't be shown in your API. For example, the `password` field on a `User` model. \n\nYou can already override a model's `toJSON()` method to support this use-case, but you have to remember to do it and when you're only hiding a single property, it feels like a lot of boilerplate.\n\nIf you define a `hidden` accessor on your model that returns an array of field names, they will automatically be omitted from JSON output.\n\n```js\nclass User extends Model {\n  id\n  username\n  password\n\n  public static hidden = ['password']\n}\n\nconst user = User.make({ username: 'user', password: 'super.secret' })\n\nJSON.stringify(user) // { \"username\": \"user\" }\n```\n\n## Concurrency control through optimistic locking\n\nImagine two requests modifying the same property on a model at the same time. Which one wins? How do you prevent this?\n\nYORM provides an easy-to-follow optimistic locking strategy through the use of versioning. Every time a Model instance is updated, it's version will be incremented. When saving, we check the local instance version with the version in the database and if they do not match, we throw a concurrency error.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdwheele%2Fyorm","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmdwheele%2Fyorm","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmdwheele%2Fyorm/lists"}