{"id":15663320,"url":"https://github.com/izelnakri/memoria","last_synced_at":"2025-06-29T21:03:26.446Z","repository":{"id":37038355,"uuid":"382820997","full_name":"izelnakri/memoria","owner":"izelnakri","description":"Single JS/TS ORM for frontend, backend \u0026 in-memory in-browser testing. Based on typeorm API, allows you to change adapters: MemoryAdapter, RESTAdapter, SQLAdapter etc.","archived":false,"fork":false,"pushed_at":"2023-07-12T18:09:44.000Z","size":4385,"stargazers_count":20,"open_issues_count":5,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-06T16:16:31.246Z","etag":null,"topics":["browser","data","database","decorators","frontend","graphql","in-memory-database","javascript","json-api","mock-server","orm","rest","rest-client","server","sql","state","state-management","testing-tools","typeorm","typescript"],"latest_commit_sha":null,"homepage":"https://memoria.surge.sh/classes/model_src.default.html#find","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/izelnakri.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2021-07-04T10:16:52.000Z","updated_at":"2025-01-21T09:25:02.000Z","dependencies_parsed_at":"2025-03-10T04:31:05.664Z","dependency_job_id":"e380e846-770d-4ce2-bd1d-a40e8a0b3531","html_url":"https://github.com/izelnakri/memoria","commit_stats":null,"previous_names":[],"tags_count":27,"template":false,"template_full_name":null,"purl":"pkg:github/izelnakri/memoria","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izelnakri%2Fmemoria","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izelnakri%2Fmemoria/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izelnakri%2Fmemoria/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izelnakri%2Fmemoria/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/izelnakri","download_url":"https://codeload.github.com/izelnakri/memoria/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izelnakri%2Fmemoria/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262667270,"owners_count":23345525,"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":["browser","data","database","decorators","frontend","graphql","in-memory-database","javascript","json-api","mock-server","orm","rest","rest-client","server","sql","state","state-management","testing-tools","typeorm","typescript"],"created_at":"2024-10-03T13:36:36.654Z","updated_at":"2025-06-29T21:03:26.414Z","avatar_url":"https://github.com/izelnakri.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"![docker-based-ci](https://github.com/izelnakri/memoria/workflows/docker-based-ci/badge.svg)\n[![codecov](https://codecov.io/gh/izelnakri/memoria/branch/main/graph/badge.svg?token=I416U3QJL7)](https://codecov.io/gh/izelnakri/memoria)\n[![npm version](https://badge.fury.io/js/@memoria%2Fmodel.svg)](https://badge.fury.io/js/@memoria%2Fmodel)\n\n# Memoria: Elegant, simple \u0026 very flexible ORM for JavaScript/TypeScript\n\nMemoria is an in-memory/off-memory state management solution for JavaScript apps on client and/or server side. It is a\nvery flexible typeorm-like entity definition API that just use JS classes and decorators to define or generate the\nschema. You can choose different adapters and use the same CRUD interface: `MemoryAdapter`, `RESTAdapter` or\n`SQLAdapter`. In other words it is a general purpose data library for JavaScript. It is also extremely useful library\nfor making frontend e2e tests extremely fast by utilizing an in-browser http server and in-memory MemoryAdapter models\nin the mock server.\n\nYou can also use it for rapid prototyping frontends for a demo: one can also use the library for single-file SPA demo\ndeployments, as a frontend SPA data store or as a backend HTTP Server ORM. The http mock server(@memoria/server) can be\nrun in-browser and node environments, thus allows for running your in-memory test suite in SSR(server-side rendering)\nenvironment if it is needed.\n\nIn summary, this is an extremely flexible and complete data management solution for JS based applications. It tries to\nbe as intuitive as it could be, without introducing new JS concepts or much boilerplates for any mutation. It is also\nvery easy to write automated tests on this framework, introspect any part of the state so it doesn't compromise on\nstability, development speed, extensibility, runtime performance \u0026 debuggability.\n\nIt is based on these principles:\n\n- TypeORM based Entity API: This makes the SQLAdapter easy to work with typeorm while making the API usable in browser\nfor frontend.\n\n- One Schema/Class that can be used in 4 environments with different Adapters: MemoryAdapter, RESTAdapter, SQLAdapter,\nGraphQLAdapter(in future maybe).\n\n- Default Model CRUD operations represented as static class methods: User.insert(), User.update() etc.\n\n- Provides ember-data like property dirty tracking on changed properties until successful CRUD operation.\n\n- Optional entity/instance based caching: Enabled by default, timeout adjustable, RESTAdapter \u0026 SQLAdapter extends from\nMemoryAdapter which provides this caching. Also useful for advanced frontend tests when used with in-browser mode of\n@memoria/server.\n\n- [Ecto Changeset](https://hexdocs.pm/ecto/Ecto.Changeset.html) inspired: Changeset structs with pipeline\noperators are very powerful. Memoria CRUD operations return ChangesetError which extends from JS Error with Ecto-like Changeset shape.\n\n## Installation\nIn order to use memoria CLI you need to have typescript set up in your project folder.\n`memoria` binary will only work on typescript project directories since it uses ts-node under the hood for\n`memoria console` and `memoria g fixtures $modelName` generation commands.\n\n``` npm install -g @memoria/cli ```\n\n``` memoria ```\n\nYou can use the CLI to create relevant boilerplate files and initial setup\n\n### memoria Model API\n\n```ts\n// memoria MODEL API\nimport Model, { primaryGeneratedColumn, Column } from '@memoria/model';\n// OR:\nconst Model = require('@memoria/model').default;\n// THEN:\n\nclass User extends Model {\n // Optionally add static Adapter = RESTAdapter; by default its MemoryAdapter\n @PrimaryGeneratedColumn()\n id: number;\n\n @Column()\n firstName: string;\n\n @Column()\n lastName: string\n\n // NOTE: you can add here your static methods\n static serializer(modelOrArray) {\n   return modelOrArray;\n }\n};\n// allows User.serializer(user);\n\nawait User.findAll(); // [];\n\nawait User.insert({ firstName: 'Izel', lastName: 'Nakri' }); // User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }\n\nlet usersAfterInsert = await User.findAll(); // [User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }]\n\nlet insertedUser = usersAfterInsert[0];\n\ninsertedUser.firstName = 'Isaac';\n\nawait User.findAll(); // [User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }]\n\nawait User.update(insertedUser); // User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }\n\nawait User.findAll(); // [User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }]\n\nlet updatedUser = await User.find(1); // User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }\n\nlet anotherUser = await User.insert({ firstName: 'Brendan' }); // User{ id: 2, firstName: 'Brendan', lastName: null }\n\nupdatedUser.firstName = 'Izel';\n\nawait User.findAll(); // [User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }, User{ id: 2, firstName: 'Brendan', lastName: null }]\n\nawait User.delete(updatedUser); // User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }\n\nawait User.findAll(); // [User{ id: 2, firstName: 'Brendan', lastName: null }]\n```\n\nNOTE: API also works for UUIDs instead of id primary keys\n\n### memoria Server API\n\n```js\n\n// in memoria/routes.ts:\n\nimport User from './models/user';\nimport Response from '@memoria/response';\n\ninterface Request {\n  headers: object,\n  params: object,\n  queryParams: object,\n  body: object\n}\n\nexport default function() {\n  this.logging = true; // OPTIONAL: only if you want to log incoming requests/responses\n  this.urlPrefix = 'http://localhost:8000/api'; // OPTIONAL: if you want to scope all the routes under a host/url\n\n  this.post('/users', async (request: Request) =\u003e {\n    const user = await User.insert(request.params.user);\n\n    return { user: User.serializer(user) };\n  });\n\n  // OR:\n  this.post('/users', User);\n\n  this.get('/users', async (request: Request) =\u003e {\n    if (request.queryParams.filter === 'is_active') {\n      const users = await User.findAll({ is_active: true });\n\n      return { users: User.serializer(users) };\n    }\n\n    return Response(422, { error: 'filter is required' });\n  });\n\n  // Shorthand without filter, displaying all users: this.get('/users', User);\n\n  this.get('/users/:id', async (request: Request) =\u003e {\n    return { user: User.serializer(await User.find(request.params.id)) };\n    // NOTE: you can wrap it with auth through custom User.findFromHeaders(request.headers) if needed.\n  });\n\n  // OR:\n  this.get('/users/:id', User);\n\n  this.put('/users/:id', async (request: Request) =\u003e {\n    let user = await User.find(request.params.id);\n\n    if (!user) {\n      return Response(404, { error: 'user not found');\n    }\n\n    return { user: User.serializer(await User.update(request.params.user)) };\n  });\n\n  // OR:\n  this.put('/users/:id', User);\n\n  this.delete('/users/:id', async ({ params }) =\u003e {\n    const user = await User.find(params.id);\n\n    if (!user) {\n      return Response(404, { errors: 'user not found' });\n    }\n\n    return await User.delete(user);\n  });\n\n  // OR:\n  this.delete('/users/:id', User);\n\n  // You can also mock APIs under different hostname\n\n  this.get('https://api.github.com/users/:username', (request) =\u003e {\n    // NOTE: your mocking logic\n  });\n\n  // OTHER OPTIONS:\n\n  this.passthrough('https://api.stripe.com');\n  // OR: this.passthrough('https://somedomain.com/api');\n\n  // OPTIONAL: this.timing(500); if you want to slow down responses for testing something etc.\n  // BookRoutes.apply(this); // if you want to apply routes from a separate file\n}\n```\n\nYou can also add routes on demand for your tests:\n\n```ts\nimport Server from './memoria/index';\nimport Response from '@memoria/response';\n\ntest('testing form submit errors when backend is down', async function (assert)  {\n\n  Server.post('/users'. (request) =\u003e {\n    return Response(500, {});\n  });\n\n  // NOTE: also there is Server.get, Server.update, Server.delete, Server.put for mocking with those verbs\n\n  await visit('/form');\n\n  // submit the form\n  // POST /users will be added to your route handlers or gets overwritten if it exists\n});\n```\n\n### memoria init/shutdown API\n\n```ts\n// in memoria/index.ts:\n\nimport memoria from \"@memoria/server\";\nimport initializer from \"./initializer\";\nimport routes from \"./routes\";\n\nconst Memoria = new memoria({\n  initializer: initializer,\n  routes: routes\n});\n\nexport default Memoria;\n\n// If you want to shutdown request mocking: Memoria.shutdown();\n// If you want to reset a database with predefined data:\n// User.resetRecords([{ id: 1, firstName: 'Izel', lastName: 'Nakri' }, { id: 2, firstName: 'Brendan', lastName: 'Eich' }]);\n```\n\nThis is basically a superior mirage.js API \u0026 implementation. Also check the tests...\n\n### memoria serializer API:\n\nmemoria serializer is very straight-forward, performant and functional/explicit. We have two ways to serialize model\ndata, it is up to you the developer if you want to serialize it in a custom format(for example JSONAPI) by adding a new\nstatic method(`static customSerializer(modelOrArray) {}`) on the model:\n\nmemoria serializer API:\n\n```js\nimport Model from '@memoria/model';\n\nclass User extends Model {\n}\n\nconst user = await User.find(1);\n\nconst serializedUserForEndpoint = { user: User.serializer(user) }; // or User.serialize(user);\n\nconst users = await User.findAll({ active: true });\n\nconst serializedUsersForEndpoint = { users: User.serializer(users) }; // or users.map((user) =\u003e User.serialize(user));\n```\n\nCustom serializers:\n\n```js\nimport Model from '@memoria/model';\n\nclass User extends Model {\n  static customSerializer(modelObjectOrArray) {\n    if (Array.isArray(objectOrArray)) {\n      return modelObjectOrArray.map((object) =\u003e this.serialize(object));\n    }\n\n    return this.customSerialize(objectOrArray);\n  }\n\n  static customSerialize(object) {\n    return Object.assign({}, object, {\n      newKey: 'something'\n    });\n  }\n}\n\nconst user = await User.find(1);\n\nconst serializedUserForEndpoint = { user: User.customSerializer(user) }; // or User.customSerialize(user);\n\nconst users = await User.findAll({ active: true });\n\nconst serializedUsersForEndpoint = { users: User.customSerializer(users) }; // or users.map((user) =\u003e User.customSerialize(user));\n```\n\n### Why this is superior to Mirage?\n\n- Class static method provide a better and more functional way to work on CRUD operations.\n\n- Better typecasting on submitted JSON data and persisted models. Empty string are `null`, '123' is a JS number, integer foreign key columns are not strings.\n\n- can run on node.js thus allows frontend mocking on server-side rendering context.\n\n- `@memoria/response` does not require `new Response`, just `Response`.\n\n- Less code output and dependencies.\n\n- No bad APIs such as association(). Better APIs, no strange factory API that introduces redundant concepts as traits,\nor implicit association behavior. Your model inserts are your factories. You can easily create different ES6 standard\nmethods on the model modules, thus memoria is easier and better to extend.\n\n- No implicit model lifecycle callbacks such as `beforeCreate`, `afterCreate`, `afterUpdate`, `beforeDelete` etc.\nThis is an old concept that is generally deemed harmful for development, we shouldn't do that extra computation during\nruntime for all CRUD. Autogenerating things after a model gets created is an implicit thus bad behavior. Validations\ncould be done in future as types or TS type decorators(like `class-validator` npm package).\n\n- route shorthands accept the model definition to execute default behavior: `this.post('/users', User)` doesn't need to dasherize,\nunderscore or do any other string manipulation to get the reference model definition. It also returns correct default\nhttp status code based on the HTTP verb, ex. HTTP POST returns 201 Created just like mirage.\n\n- very easy to debug/develop the server, serialize any data in a very predictable and functional way.\n\n- API is very similar to Mirage, it can do everything mirage can do, while all redudant and worse API removed.\n\n- written in Typescript, thus provides type definitions by default.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizelnakri%2Fmemoria","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fizelnakri%2Fmemoria","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizelnakri%2Fmemoria/lists"}