{"id":13629944,"url":"https://github.com/thebinarysearchtree/flyweight","last_synced_at":"2025-04-17T13:30:53.780Z","repository":{"id":60267634,"uuid":"529831205","full_name":"thebinarysearchtree/flyweight","owner":"thebinarysearchtree","description":"An ORM for SQLite","archived":false,"fork":false,"pushed_at":"2024-05-23T22:30:09.000Z","size":1394,"stargazers_count":516,"open_issues_count":3,"forks_count":7,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-08-01T22:44:56.735Z","etag":null,"topics":["nodejs","orm","sqlite"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/thebinarysearchtree.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-28T10:37:14.000Z","updated_at":"2024-07-30T13:47:26.000Z","dependencies_parsed_at":"2023-02-17T00:45:21.536Z","dependency_job_id":"486d6b86-9c9d-4d40-950e-7c30e32cd463","html_url":"https://github.com/thebinarysearchtree/flyweight","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/thebinarysearchtree%2Fflyweight","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thebinarysearchtree%2Fflyweight/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thebinarysearchtree%2Fflyweight/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thebinarysearchtree%2Fflyweight/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thebinarysearchtree","download_url":"https://codeload.github.com/thebinarysearchtree/flyweight/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223757071,"owners_count":17197490,"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":["nodejs","orm","sqlite"],"created_at":"2024-08-01T22:01:25.030Z","updated_at":"2025-04-17T13:30:53.763Z","avatar_url":"https://github.com/thebinarysearchtree.png","language":"JavaScript","readme":"# Flyweight\nFlyweight is a NodeJS and edge ORM with first-class support for databases that are compatible with SQLite, including Cloudflare D1 and Turso.\n\nFeatures include a comprehensive API, the ability to automatically type and query inside JSON, and advanced typing of raw SQL queries so that you are not without TypeScript support in any situation.\n\n## Creating tables\n\nTables are created the same way as they are in SQL. The native types available in strict mode are ```integer```, ```real```, ```text```, ```blob```, and ```any```. In addition to these types, four additional types are included by default: ```boolean```, ```date```, and ```json```. ```boolean``` is a column in which the values are restricted to 1 or 0, ```date``` is a JavaScript ```Date``` stored as an ISO8601 string, and ```json``` is ```jsonb``` stored as a blob if the database supports it, otherwise it is text. These additional types are automatically parsed by the ORM.\n\n```sql\ncreate table events (\n    id integer primary key,\n    name text not null,\n    startTime date not null,\n    locationId integer references locations\n);\n```\n\nIf you want to get one row with the basic API, you can use:\n\n```js\nconst event = await db.events.get({ id: 100 });\n```\n\nIf you want to get many rows, you can use:\n\n```js\nconst names = await db.events.many({ id: eventIds }, 'name');\n```\n\nIf you want to insert a row, you can do:\n\n```js\nconst id = await db.coaches.insert({\n  name: 'Eugene Bareman',\n  city: 'Auckland'\n});\n```\n\n## Getting started\n\n```\nmkdir test\ncd test\nnpm init\nnpx create-flyweight database\n```\n\nYou can run the ```npx``` command at the root of either an existing or a new project. Once that is done, you can import the database this way:\n\n```js\nimport { db } from './database/db.js';\n\nawait db.users.insert({ name: 'Andrew' });\nconst users = await db.users.many();\nconsole.log(users);\n```\n\nA ```users``` table has already been created for you to play around with.\n\nYou can update types whenever you change the SQL by either calling ```npm run watch``` to automatically update the types, or ```npm run types``` to do it manually.\n\n## Migrations\n\nTables are defined in ```./database/sql/tables.sql```. You can add or change tables from here and then run the migration command ```npm run migrate \u003cmigration-name\u003e```.\n\nIf you want to reset the migration system to a new database that already has tables created on it, edit the ```tables.sql``` file and then run ```npm run reset```.\n\nIf you want to add a new column to a table without needing to drop the table, make sure you put the column at the end of the list of columns.\n\n## JSON support\n\nFlyweight can sample columns that are declared with the ```json``` by querying the database. From these samples, types will be automatically created for both the return type of queries and for creating queries themselves.\n\nTo sample your local database, run ```npm run sample```.\n\n## Default values\n\nDefault values can be set for boolean and date columns using the following syntax:\n\n```sql\ncreate table users (\n  id integer primary key,\n  isDisabled boolean not null default false,\n  createdAt date not null default now()\n);\n```\n\n```current_timestamp``` will not work properly when wanting to set the default date to the current time. This is because ```current_timestamp``` does not include timezone information and therefore when parsing the date string from the database, JavaScript will assume it is in local time when it is in fact in UTC time.\n\n## The API\n\nEvery table has ```get```, ```many```, ```query```, ```update```, ```upsert```, ```insert```, ```insertMany```, and ```remove``` methods available to it, along with any of the custom methods that are created when you add a new SQL file to the corresponding table's folder. Views only have the ```get```, ```many```, and ```query``` methods available to them.\n\n### Insert\n\n```insert``` simply takes one argument - ```params```, with the keys and values corresponding to the column names and values you want to insert. It returns the primary key, or part of the primary key if the table has a composite primary key. For batch inserts you can use ```insertMany``` and it takes an array of ```params```. It doesn't return anything.\n\n### Update\n\n```update``` takes an object with an optional ```where``` property, and a ```set``` property. It returns a number representing the number of rows that were affected by the query. For example:\n\n```js\nawait db.coaches.update({\n  where: { id: 100 }, \n  set: { city: 'Brisbane' }\n});\n```\n\nwhich corresponds to\n\n```sql\nupdate coaches set city = 'Brisbane' where id = 100;\n```\n\n### Upsert\n\n```upsert``` will update the row if the target's uniqueness contraint is violated by the insert. If ```target``` or ```set``` are not provided, the upsert will do nothing when there is a conflict. ```upsert``` returns the primary key of the inserted or updated row.\n\n```js\nconst id = await db.coaches.upsert({\n  values: {\n    id: 1,\n    name: 'Test User',\n    city: 'Test City'\n  },\n  target: 'id',\n  set: {\n    city: 'Updated City'\n  }\n});\n```\n\n### Get and Many\n\n```get``` and ```many``` take two optional arguments. The first is ```params``` - an object representing the where clause. For example:\n\n```js\nconst fights = await db.fights.many({ cardId: 9, titleFight: true });\n```\n\ntranslates to\n\n```sql\nselect * from fights where cardId = 9 and titleFight = 1;\n```\n\nThe keys to ```params``` must be the column names of the table. The values can either be of the same type as the column, an array of values that are the same type as the column or null. If an array is passed in, an ```in``` clause is used, such as:\n\n```js\nconst fights = await db.fights.many({ cardId: [1, 2, 3] });\n```\n\nwhich translates to\n\n```sql\nselect * from fights where cardId in (1, 2, 3);\n```\n\nIf null is passed in as the value, the SQL will use ```is null```.\n\nAll of the arguments are passed in as parameters for security reasons.\n\nThe second argument to ```get``` or ```many``` selects which columns to return. It can be one of the following:\n\n1. a string representing a column to select. In this case, the result returned is a single value or array of single values, depending on whether ```get``` or ```many``` is used.\n\n```js\nconst born = await db.fighters.get({ id: 3 }, 'born');\n```\n\n2. a lambda function that traverses a JSON object.\n\n```js\nconst instagram = await db.fighters.get({ id: 3 }, c =\u003e c.social.instagram);\n```\n\nIn this case, ```social``` is a JSON object with an ```instagram``` property.\n\n3. an array of strings, representing the columns to select.\n\n```js\nconst fighter = await db.fighters.get({ id: 3 }, ['id', 'born']);\n```\n\n### Query and First\n\nYou can use the ```query``` or ```first``` syntax for more complex queries. ```query``` returns an array in the same way as ```many```, and ```first``` returns an object or ```undefined``` if nothing is found. The additional keywords are:\n\n```select```: a string or array of strings representing the columns to select.\n\n```omit```: a s tring or array of strings representing the columns to omit. All of the other columns will be selected.\n\n```include```: include other tables in the result.\n\n```alias```: create an alias for columns, such as when selecting inside of JSON objects.\n\n```orderBy```: a string representing the column to order the result by, or an array of columns to order the result by.\n\n```desc```: set to true when using ```orderBy``` if you want the results in descending order.\n\n```limit``` and ```offset```: corresponding to the SQL keywords with the same name.\n\n```distinct```: adds the ```distinct``` keywords to the start of the select clause.\n\n```debug```: when set to true, the result will include debug information such as the raw SQL used in the query.\n\nFor example:\n\n```js\nconst fighters = await db.fighters.query({\n  where: { \n    isActive: true \n  }, \n  select: ['name', 'hometown'],\n  orderBy: 'reachCm',\n  limit: 10\n});\n```\n\nYou can select inside JSON objects and create an alias for the result:\n\n```js\nconst orderBy = await db.fighters.query({\n  where: {\n    id: n =\u003e n.lt(10)\n  },\n  select: ['id', 'born'],\n  alias: {\n    instagram: s =\u003e s.social.instagram\n  },\n  orderBy: 'instagram'\n});\n```\n\nYou can also include additional relations:\n\n```js\nconst locations = await db.locations.query({\n  include: {\n    events: (t, c) =\u003e t.events.many({ locationId: c.id })\n  }\n});\n```\n\nIf you want to predefine the join clause for two tables, you can call ```define```. This should be done in the ```db.js``` file before exporting.\n\n```js\ndb.locations.define((t, c) =\u003e t.events.where({\n  locationId: c.id\n}));\n```\n\nNow you no longer have to specify the join in queries.\n\n```js\nconst defined = await db.locations.query({\n  include: {\n    events: t =\u003e t.events.query({\n      limit: 3,\n      orderBy: 'startTime',\n      desc: true\n    })\n  },\n  limit: 3\n});\n```\n\nWhile the default interpretation of the query parameters is ```=```, you can pass in a function to use ```not```, ```gt```, ```gte```, ```lt```, ```lte```, ```like```, ```range```, ```match``` and ```glob```.\n\nFor example:\n\n```js\nconst excluded = [1, 2, 3];\nconst users = await db.users.many({ id: i =\u003e i.not(excluded) });\nconst count = await db.users.count({\n  where: {\n    id: i =\u003e i.range({ gt: 10, lt: 15 })\n  }\n});\n```\n\n### Complex filtering\n\nIf you need to perform complex logic in the ```where``` clause, you can use the ```and``` or ```or``` properties. For example:\n\n```js\nconst events = await db.events.query({\n  where: {\n    or: [\n      { name: n =\u003e n.like('UFC 1_: The%') },\n      { id: n =\u003e n.lt(10) },\n      {\n        and: [\n          { startTime: n =\u003e n.gt(time) },\n          { name: n =\u003e n.like('%Japan%') }\n        ]\n      }\n    ]\n  }\n});\n```\n\nYou should only include one condition per object.\n\n### Aggregate functions\n\nThere are multiple functions that aggregate the results into a single value. These include ```count```, ```avg```, ```min```, ```max```, and ```sum```. Despite its name, ```sum``` uses the SQLite function ```total``` to determine the results.\n\nAll of these functions take three arguments:\n\n```where```: the where clause\n```column```: the column to aggregate. This is optional for ```count```.\n```distinct```: the same as ```column``` but it adds ```distinct``` to the front.\n\n```js\nconst count = await db.fighters.count({\n  where: {\n    hometown: 'Brisbane, Australia'\n  }\n});\n```\n\nThere is also an ```exists``` function that takes one argument representing the where clause.\n\n```js\nconst exists = await db.fighters.exists({ name: 'Israel Adesanya' });\n```\n\n### Remove\n\n```remove``` takes one argument representing the where clause and returns the number of rows affected by the query.\n\n```js\nconst changes = await db.fighters.remove({ id: 100 });\n```\n\n## Creating SQL queries\n\nWhen the API doesn't do what you need it to do, you can create SQL queries. You can do this by creating a folder with the same name as the table, such as ```./database/sql/users```. You can then put SQL files in this folder that will be available in the API.\n\nFor example, if you create a query in ```./database/sql/users/roles.sql``` that looks like this:\n\n```sql\nselect\n    u.id,\n    u.name,\n    groupArray(r.name) as roles\nfrom\n    users u join\n    userRoles ur on ur.userId = u.id join\n    roles r on ur.roleId = r.id\nwhere \n    u.name = $name\ngroup by \n    u.id\n```\n\nA function ```db.users.roles``` will be available in the API with the correct types.\n\n![auto-completed code](hero.png)\n\nWhen creating SQL queries, make sure you give an alias to any columns in the select statement that don't have a name. For example, do not do:\n\n```sql\nselect max(startTime) from events;\n```\n\nas there is no name given to ```max(startTime)```.\n\nParameters in SQL files should use the ```$name``` notation. If you want to include dynamic content that cannot be parameterized, you should use the ```${column}``` format and then pass in a second argument when calling the SQL statement in JavaScript. For example:\n\n```sql\nselect * from users where location = $location order by ${column};\n```\n\n```js\nconst options = {\n  unsafe: {\n    column: 'lastName'\n  }\n};\nconst users = await db.users.from({ location: 'Brisbane' }, options);\n```\n\nIf the unsafe parameter is ```undefined``` in the options argument, it will be removed from the SQL statement.\n\nSingle quotes in strings should be escaped with ```\\```. JSON functions are automatically typed and parsed. For example, the following:\n\n```sql\nselect id, object(name, startTime) as nest from events;\n```\n\nwill have the type:\n\n```ts\ninterface EventQuery {\n  id: number;\n  nest: { name: string, startTime: Date }\n}\n```\n\nNulls are automatically removed from all ```groupArray``` results. When all of the properties of ```object``` are from a left or right join, and there are no matches from that table, instead of returning, for example:\n\n```js\n{ name: null, startTime: null }\n```\n\nthe entire object will be null.\n\n## Shorthand JSON functions\n\n```sql\nobject(\n    u.id, \n    u.name, \n    u.social) as user\n``` \n\nis just shorthand for \n\n```sql\njson_object(\n    'id', u.id, \n    'name', u.name, \n    'social', u.social) as user\n```\n\nOther commands available are ```groupArray``` which is shorthand for ```json_group_array```, and ```array```, which is shorthand for ```json_array```.\n\n## Alias stars\n\nNormally, SQLite doesn't support aliased stars, but this syntax is now available when writing SQL statements with Flyweight.\n\n```sql\nselect\n    e.*,\n    l.name as locationName\nfrom \n    events e join\n    locations l on e.locationId = l.id\n```\n\n## Transactions\n\nTransactions involve locking writes to the database with ```getTransaction```. If multiple transactions try to run at the same time, they will wait until the current transaction is complete.\n\n```js\nimport { db } from './db.js';\n\ntry {\n  const tx = await db.getTransaction();\n  await tx.begin();\n\n  const coachId = await tx.coaches.insert({\n    name: 'Eugene Bareman',\n    city: 'Auckland'\n  });\n  const fighterId = await tx.fighters.get({ name: n =\u003e n.like('Israel%') }, 'id');\n  await tx.fighterCoaches.insert({\n    fighterId,\n    coachId\n  });\n  \n  await tx.commit();\n}\ncatch (e) {\n  console.log(e);\n  await tx.rollback();\n}\n```\n\n## Batches\n\nYou can also run multiple statements inside a single transaction without any logic using ```batch```. This is supported by all databases. Here is an example using D1.\n\n```ts\nimport createClient from './database/db';\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise\u003cResponse\u003e {\n    const db = createClient(env.DB);\n\n    const projectId = 1;\n    const [project, tags, issues] = await db.batch((bx) =\u003e [\n      bx.projects.get({ id: projectId }),\n      bx.tags.many({ projectId }),\n      bx.issues.many({ projectId })\n    ]);\n\n    return Response.json({\n      ...project\n      tags,\n      issues\n    });\n  }\n};\n```\n\n## Views\n\nViews are treated like read-only tables. They have a ```get``` and ```many``` method available to them that works the same as with tables. If you want to create a view called ```activeUsers``` you can add a file in the ```views``` folder called ```./database/views/activeUsers.sql``` that might have SQL like this:\n\n```sql\ncreate view activeUsers as\nselect * from users where isActive = true;\n```\n\nYou can now use it in the API like this:\n\n```js\nimport { db } from './database/db.js';\n\nconst user = await db.activeUsers.get({ id: 100 }, ['name', 'email']);\nconsole.log(user.email);\n```\n\n## Cloudflare D1\n\nFlyweight provides first-class support for D1. The only difference between the D1 API and the SQLite API is that D1 doesn't support transactions other than ```batch```.\n\nTo get started, run this command in the root of your Cloudflare Workers project.\n\n```\nnpx create-flyweight d1 src/database\n```\n\nThe first thing you will want to do is go into ```src/database/config.js``` and set the database name. If you want to use JSON sampling, you should also set the ```localPath``` to the path of the local SQLite file, which is usually somewhere in the ```.wrangler``` folder.\n\nIf your database already has tables created on it, go into ```src/database/sql/tables.sql``` and add all of the ```create``` statements and then run:\n\n```\nnpm run reset\n```\n\nto reset the migration system to the current state of the database. All migration commands work on the local version of the database and interface with the wrangler migration system so that you can run ```apply``` on the remote database yourself to add any migrations.\n\nIf you have more than one database and want to create a migration for a specific database, you can run:\n\n```\nnpm run migrate dbName migrationName\n```\n\nYou should run ```npm run watch``` to keep the ```src/database/files.js``` updated with any new sql files or table changes while you write the code.\n\n## Turso\n\nTurso uses ```npm run watch``` to keep the same file D1 uses updated so that the database can run in edge-based environments where necessary. Turso also supports the same transaction API that the standard SQLite database uses. The only difference is that the ```getTransaction``` function for Turso needs a type of either ```read``` or ```write```.\n\nIn the root directory of the project, you can install flyweight with\n\n```\nnpx create-flyweight turso database\n```\n\nYou will then need to edit the file in ```database/db.js``` to change the ```url``` and any other arguments you need. You will also want to change the import statement for turso to use the web version of the client if you are running in a edge-based environment.\n\nYou can then use it like this:\n\n```ts\nimport createClient from './database/db';\n\nexport default {\n  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise\u003cResponse\u003e {\n    const db = createClient();\n    const users = await db.users.many();\n\n    return Response.json(users);\n  }\n};\n```\n\n\n\n## Running tests\n\nTo run the tests, first go into the ```test``` folder and run ```node setup.js``` to move the test database to the right location. You can then run the tests with ```node test.js``` or ```npm test```.\n","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthebinarysearchtree%2Fflyweight","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthebinarysearchtree%2Fflyweight","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthebinarysearchtree%2Fflyweight/lists"}