{"id":16139905,"url":"https://github.com/nathanhoad/nathanhoad-data","last_synced_at":"2025-04-06T18:18:59.439Z","repository":{"id":42510098,"uuid":"245559553","full_name":"nathanhoad/nathanhoad-data","owner":"nathanhoad","description":"A small ORM that uses Typescript and plain objects. No fancy instance methods - all queries are class methods.","archived":false,"fork":false,"pushed_at":"2023-10-19T11:58:54.000Z","size":339,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-13T00:17:03.514Z","etag":null,"topics":["database","orm","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nathanhoad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2020-03-07T03:13:54.000Z","updated_at":"2023-07-25T14:33:34.000Z","dependencies_parsed_at":"2024-11-01T16:40:48.463Z","dependency_job_id":"36bf1a93-a7eb-4a0f-9dcd-6e27dc1e6469","html_url":"https://github.com/nathanhoad/nathanhoad-data","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanhoad%2Fnathanhoad-data","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanhoad%2Fnathanhoad-data/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanhoad%2Fnathanhoad-data/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nathanhoad%2Fnathanhoad-data/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nathanhoad","download_url":"https://codeload.github.com/nathanhoad/nathanhoad-data/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247526772,"owners_count":20953143,"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":["database","orm","typescript"],"created_at":"2024-10-09T23:50:07.476Z","updated_at":"2025-04-06T18:18:59.409Z","avatar_url":"https://github.com/nathanhoad.png","language":"TypeScript","readme":"# @nathanhoad/data\n\nA small ORM that uses Typescript and plain objects.\n\nModel instances have no fancy database methods. All querying is done statically from the model's class.\n\n`npm i @nathanhoad/data`\n\n## Migration CLI\n\n### Generating migrations\n\n`npx data migration \u003cmigration-name\u003e`\n\nFor example, `npx data migration create-things` will create a migration file that creates the table `things` with an `id`, `createdAt`, and `updatedAt` fields. Add in any other fields and indices that you need but those three fields are the minimum for models to function correctly.\n\nUsing something like `npx data migration add-name-to-things` will create a migration that adds a `name` field to `things`. You can add multipled fields with something like `add-this-and-that-to-things`. It will try its best to guess what you mean but doesn't always get it right.\n\n### Running migrations\n\nRun any pending migrations with:\n\n`npx data up`\n\nAnd rollback the last migration group:\n\n`npx data down`\n\nTo list all migrations, both pending and completed, run:\n\n`npx data list`\n\n### Schema\n\nGet the schema for all tables or just one:\n\n`npx data schema [table]`\n\nTo get the basic types for a table:\n\n`npx data types [table]`\n\n## Models\n\nThe easiest init is:\n\n```ts\nimport data from \"@nathanhoad/data\";\n```\n\n`data` is a function that returns a connected instance of `Database`.\n\nIt assumes that `process.env.DATABASE_URL` or `process.env.TEST_DATABASE_URL` is set.\n\nIf you want to manually connect you can create your own wrapper:\n\n```ts\nimport { Database } from '@nathanhoad/data';\n\n// tell the database to not connect automatically\nconst database = new Database(false);\n// connect accepts a connection string or a knex object\ndatabase.connect(process.env.DATABASE_URL);\nexport database;\n```\n\nA bigger example:\n\n```ts\nimport data from \"@nathanhoad/data\";\n\nimport { IProjectModel } from \"./Projects\";\nimport { IShirtModel } from \"./Shirts\";\n\nexport interface IUserModel {\n  id?: string;\n  firstName?: string;\n  lastName?: string;\n  slug?: string;\n  createdAt?: Date;\n  updatedAt?: Date;\n\n  projects?: Array\u003cIProjectModel\u003e;\n  department?: IDepartmentModel;\n  shirts?: Array\u003cIShirtModel\u003e;\n}\n\nexport default data().model\u003cIUserModel\u003e(\"users\", {\n  hooks: {\n    beforeCreate(user) {\n      user.slug = slugify(`${user.firstName} ${user.lastName}`, 6);\n    }\n  },\n  relations: {\n    projects: { hasAndBelongsToMany: \"projects\" },\n    department: { belongsTo: \"department\" }, // assumes departmentId\n    shirts: { hasMany: \"shirts\", dependent: true } // deleting this user will delete all of their shirts\n  },\n  contexts: {\n    simple: [\"id\", \"fullName\"], // only these fields are included in the resulting object\n    derived(user) {\n      // Add and remove properties to be sent\n      user.activeFor = new Date() - user.createdAt;\n      delete user.createdAt;\n      delete user.updatedAt;\n      return user;\n    }\n  }\n});\n```\n\n### Hooks\n\nModels expose a few hooks to help you manage the data going into the database.\n\nThose hooks are (and generally called in this order):\n\n- `beforeCreate` - Called before a model is created.\n- `beforeSave` - Called before a model is saved (including just before it is created)\n- `afterSave` - Called just after a model is saved (including just after it was created)\n- `afterCreate` - Called just after a model is created\n- `beforeDestroy` - Called just before a model is deleted\n- `afterDestroy` - Called just after a model is deleted\n\nHooks are just functions and take the model (minus any relations) as the first argument.\n\n```ts\nconst Things = data().model\u003cIThingModel\u003e(\"users\", {\n  hooks: {\n    beforeCreate(user) {\n      user.slug = slugify(`${user.firstName} ${user.lastName}`, 6);\n    }\n  }\n});\n```\n\nYou can also return a promise:\n\n```ts\nconst Things = data().model\u003cIThingModel\u003e(\"users\", {\n  hooks: {\n    async beforeCreate(user) {\n      user.slug = await getSomeValue();\n    }\n  }\n});\n```\n\n#### `id`, `createdAt` and `updatedAt`\n\nAll models are assumed to have `id`, `createdAt`, and `updatedAt` defined on them in the database.\n\n### Creating\n\n`.create()` is just an alias for `.save()`.\n\n```ts\nconst Users = data().model\u003cIUserModel\u003e(\"users\");\n\nUsers.create({ firstName \"Nathan\" }).then(user =\u003e {\n  user.firstName; // =\u003e Nathan\n});\n\nUsers.create([{ firstName \"Nathan\" }, { firstName \"Lilly\" }]).then(users =\u003e {\n  users.length; // =\u003e 2\n});\n```\n\n### Finding\n\n```ts\nUsers.where({ email: \"test@test.com\" })\n  .first()\n  .then(user =\u003e {\n    // user is an instance of Immutable.Map\n    user.firstName = \"Test\";\n\n    Users.save(user).then(updatedUser =\u003e {\n      user.get(\"name\");\n    });\n  });\n\nUsers.find(\"167a6f71-4e0f-4fb4-b2e8-a6dd2f5d087e\").then(user =\u003e {\n  user.id; // 167a6f71-4e0f-4fb4-b2e8-a6dd2f5d087e\n});\n\nUsers.all().then(users =\u003e {\n  Users.withContext(users);\n});\n```\n\n### Saving\n\n```ts\nuser.firstName = \"Nathan\";\nUsers.save(user).then(user =\u003e {\n  user.updatedAt; // Just then\n});\n```\n\nSaving a model that has `relations` attached will also attempt to save the attached related rows.\n\n### Destroying\n\n```ts\nUsers.destroy(user).then(user =\u003e {\n  // user is the user that was just destroyed\n});\n```\n\nAny dependent related records will also be destroyed (see down further in Associations/Relations).\n\n## Applying context\n\nModels can be converted to generic objects (for example, as the final step of an API endpoing response) by given an array of fields or providing a context mapper function.\n\nContexts are defined on the model:\n\n```ts\nconst Users = data().model\u003cIUserModel\u003e(\"users\", {\n  contexts: {\n    simple: [\"id\", \"fullName\"], // only these fields are included in the resulting object\n    derived(user) {\n      // Add and remove properties to be sent\n      user.activeFor = new Date() - user.createdAt;\n      delete user.createdAt;\n      delete user.updatedAt;\n      return user;\n    }\n  }\n});\n\n// Arrays\nUsers.withContext(users); // \"default\" context will just return the object unless defined\nUsers.withContext(users, \"simple\");\nUsers.withContext(users, \"derived\");\nUsers.withContext(users);\n\n// Single objects\nUsers.withContext(user);\nUsers.withContext(user, \"simple\");\nUsers.withContext(user, \"derived\");\n```\n\n## Relations/Associations\n\nDefine `relations` on the collection:\n\n```ts\nconst Users = data().model\u003cIUserModel\u003e(\"users\", {\n  relations: {\n    projects: { hasAndBelongsToMany: \"projects\" },\n    department: { belongsTo: \"department\" }, // assumes departmentId unless otherwise specified\n    shirts: { hasMany: \"shirts\", dependent: true } // deleting this user will delete all of their shirts\n  }\n});\n```\n\nSet them on a model and save them. Anything that hasn't already been saved will be saved.\n\n```ts\nlet newProject = {\n  name: \"Some cool project\"\n};\n\nlet newUser = {\n  name: \"Nathan\",\n  projects: [new_project]\n};\n\nUsers.create(newUser).then(user =\u003e {\n  user.projects; // array containing saved newProject\n});\n```\n\nAnd then retrieve them.\n\n```ts\nUsers.include(\"projects\")\n  .all()\n  .then(users =\u003e {\n    users[0].projects; // array of projects\n  });\n```\n\nYou can specify the key fields and table if needed:\n\n```ts\nconst Users = data().model\u003cIUserModel\u003e(\"users\", {\n  relations: {\n    projects: {\n      hasAndBelongsToMany: \"projects\",\n      through: \"project_people\",\n      primaryKey: \"user_id\",\n      foreignKey: \"project_id\"\n    },\n    department: { belongsTo: \"department\", foreignKey: \"department_id\", table: \"department\" },\n    shirts: { has_many: \"shirts\", dependent: true, foreignKey: \"user_id\" }\n  }\n});\n```\n\n## Transactions\n\nTo wrap your actions inside a transaction just call:\n\n```ts\nimport data from \"@nathanhoad/data\";\n\nconst Users = data().model\u003cIUserModel\u003e(\"users\");\nconst Hats = data().model\u003cIHatModel\u003e(\"hats\");\n\ndata()\n  .transaction(async transaction =\u003e {\n    const user = await Users.create({ firstName: \"Nathan\" }, { transaction });\n    const hat = await Hats.create({ type: \"Cowboy\" }, { transaction });\n  })\n  .then(() =\u003e {\n    // User and Hat are both committed to the database now\n  })\n  .catch(err =\u003e {\n    // Something failed and both User and Hat are now rolled back\n  });\n```\n\n## Contributors\n\n- Nathan Hoad - [nathan@nathanhoad.net](mailto:nathan@nathanhoad.net)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanhoad%2Fnathanhoad-data","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnathanhoad%2Fnathanhoad-data","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnathanhoad%2Fnathanhoad-data/lists"}