- Notifications
You must be signed in to change notification settings - Fork0
Single JS/TS ORM for frontend, backend & in-memory in-browser testing. Based on typeorm API, allows you to change adapters: MemoryAdapter, RESTAdapter, SQLAdapter etc.
License
izelnakri/memoria
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Memoria is an in-memory/off-memory state management solution for JavaScript apps on client and/or server side. It is avery flexible typeorm-like entity definition API that just use JS classes and decorators to define or generate theschema. You can choose different adapters and use the same CRUD interface:MemoryAdapter
,RESTAdapter
orSQLAdapter
. In other words it is a general purpose data library for JavaScript. It is also extremely useful libraryfor making frontend e2e tests extremely fast by utilizing an in-browser http server and in-memory MemoryAdapter modelsin the mock server.
You can also use it for rapid prototyping frontends for a demo: one can also use the library for single-file SPA demodeployments, as a frontend SPA data store or as a backend HTTP Server ORM. The http mock server(@memoria/server) can berun in-browser and node environments, thus allows for running your in-memory test suite in SSR(server-side rendering)environment if it is needed.
In summary, this is an extremely flexible and complete data management solution for JS based applications. It tries tobe as intuitive as it could be, without introducing new JS concepts or much boilerplates for any mutation. It is alsovery easy to write automated tests on this framework, introspect any part of the state so it doesn't compromise onstability, development speed, extensibility, runtime performance & debuggability.
It is based on these principles:
TypeORM based Entity API: This makes the SQLAdapter easy to work with typeorm while making the API usable in browserfor frontend.
One Schema/Class that can be used in 4 environments with different Adapters: MemoryAdapter, RESTAdapter, SQLAdapter,GraphQLAdapter(in future maybe).
Default Model CRUD operations represented as static class methods: User.insert(), User.update() etc.
Provides ember-data like property dirty tracking on changed properties until successful CRUD operation.
Optional entity/instance based caching: Enabled by default, timeout adjustable, RESTAdapter & SQLAdapter extends fromMemoryAdapter which provides this caching. Also useful for advanced frontend tests when used with in-browser mode of@memoria/server.
Ecto Changeset inspired: Changeset structs with pipelineoperators are very powerful. Memoria CRUD operations return ChangesetError which extends from JS Error with Ecto-like Changeset shape.
In order to use memoria CLI you need to have typescript set up in your project folder.memoria
binary will only work on typescript project directories since it uses ts-node under the hood formemoria console
andmemoria g fixtures $modelName
generation commands.
npm install -g @memoria/cli
memoria
You can use the CLI to create relevant boilerplate files and initial setup
// memoria MODEL APIimportModel,{primaryGeneratedColumn,Column}from'@memoria/model';// OR:constModel=require('@memoria/model').default;// THEN:classUserextendsModel{// Optionally add static Adapter = RESTAdapter; by default its MemoryAdapter @PrimaryGeneratedColumn()id:number; @Column()firstName:string; @Column()lastName:string// NOTE: you can add here your static methodsstaticserializer(modelOrArray){returnmodelOrArray;}};// allows User.serializer(user);awaitUser.findAll();// [];awaitUser.insert({firstName:'Izel',lastName:'Nakri'});// User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }letusersAfterInsert=awaitUser.findAll();// [User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }]letinsertedUser=usersAfterInsert[0];insertedUser.firstName='Isaac';awaitUser.findAll();// [User{ id: 1, firstName: 'Izel', lastName: 'Nakri' }]awaitUser.update(insertedUser);// User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }awaitUser.findAll();// [User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }]letupdatedUser=awaitUser.find(1);// User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }letanotherUser=awaitUser.insert({firstName:'Brendan'});// User{ id: 2, firstName: 'Brendan', lastName: null }updatedUser.firstName='Izel';awaitUser.findAll();// [User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }, User{ id: 2, firstName: 'Brendan', lastName: null }]awaitUser.delete(updatedUser);// User{ id: 1, firstName: 'Isaac', lastName: 'Nakri' }awaitUser.findAll();// [User{ id: 2, firstName: 'Brendan', lastName: null }]
NOTE: API also works for UUIDs instead of id primary keys
// in memoria/routes.ts:importUserfrom'./models/user';importResponsefrom'@memoria/response';interfaceRequest{headers:object,params:object,queryParams:object,body:object}exportdefaultfunction(){this.logging=true;// OPTIONAL: only if you want to log incoming requests/responsesthis.urlPrefix='http://localhost:8000/api';// OPTIONAL: if you want to scope all the routes under a host/urlthis.post('/users',async(request:Request)=>{constuser=awaitUser.insert(request.params.user);return{user:User.serializer(user)};});// OR:this.post('/users',User);this.get('/users',async(request:Request)=>{if(request.queryParams.filter==='is_active'){constusers=awaitUser.findAll({is_active:true});return{users:User.serializer(users)};}returnResponse(422,{error:'filter is required'});});// Shorthand without filter, displaying all users: this.get('/users', User);this.get('/users/:id',async(request:Request)=>{return{user:User.serializer(awaitUser.find(request.params.id))};// NOTE: you can wrap it with auth through custom User.findFromHeaders(request.headers) if needed.});// OR:this.get('/users/:id',User);this.put('/users/:id',async(request:Request)=>{letuser=awaitUser.find(request.params.id);if(!user){returnResponse(404,{error:'user not found');}return{user:User.serializer(awaitUser.update(request.params.user))};});// OR:this.put('/users/:id',User);this.delete('/users/:id',async({ params})=>{constuser=awaitUser.find(params.id);if(!user){returnResponse(404,{errors:'user not found'});}returnawaitUser.delete(user);});// OR:this.delete('/users/:id',User);// You can also mock APIs under different hostnamethis.get('https://api.github.com/users/:username',(request)=>{// NOTE: your mocking logic});// OTHER OPTIONS:this.passthrough('https://api.stripe.com');// OR: this.passthrough('https://somedomain.com/api');// OPTIONAL: this.timing(500); if you want to slow down responses for testing something etc.// BookRoutes.apply(this); // if you want to apply routes from a separate file}
You can also add routes on demand for your tests:
importServerfrom'./memoria/index';importResponsefrom'@memoria/response';test('testing form submit errors when backend is down',asyncfunction(assert){Server.post('/users'.(request)=>{returnResponse(500,{});});// NOTE: also there is Server.get, Server.update, Server.delete, Server.put for mocking with those verbsawaitvisit('/form');// submit the form// POST /users will be added to your route handlers or gets overwritten if it exists});
// in memoria/index.ts:importmemoriafrom"@memoria/server";importinitializerfrom"./initializer";importroutesfrom"./routes";constMemoria=newmemoria({initializer:initializer,routes:routes});exportdefaultMemoria;// If you want to shutdown request mocking: Memoria.shutdown();// If you want to reset a database with predefined data:// User.resetRecords([{ id: 1, firstName: 'Izel', lastName: 'Nakri' }, { id: 2, firstName: 'Brendan', lastName: 'Eich' }]);
This is basically a superior mirage.js API & implementation. Also check the tests...
memoria serializer is very straight-forward, performant and functional/explicit. We have two ways to serialize modeldata, it is up to you the developer if you want to serialize it in a custom format(for example JSONAPI) by adding a newstatic method(static customSerializer(modelOrArray) {}
) on the model:
memoria serializer API:
importModelfrom'@memoria/model';classUserextendsModel{}constuser=awaitUser.find(1);constserializedUserForEndpoint={user:User.serializer(user)};// or User.serialize(user);constusers=awaitUser.findAll({active:true});constserializedUsersForEndpoint={users:User.serializer(users)};// or users.map((user) => User.serialize(user));
Custom serializers:
importModelfrom'@memoria/model';classUserextendsModel{staticcustomSerializer(modelObjectOrArray){if(Array.isArray(objectOrArray)){returnmodelObjectOrArray.map((object)=>this.serialize(object));}returnthis.customSerialize(objectOrArray);}staticcustomSerialize(object){returnObject.assign({},object,{newKey:'something'});}}constuser=awaitUser.find(1);constserializedUserForEndpoint={user:User.customSerializer(user)};// or User.customSerialize(user);constusers=awaitUser.findAll({active:true});constserializedUsersForEndpoint={users:User.customSerializer(users)};// or users.map((user) => User.customSerialize(user));
Class static method provide a better and more functional way to work on CRUD operations.
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.can run on node.js thus allows frontend mocking on server-side rendering context.
@memoria/response
does not requirenew Response
, justResponse
.Less code output and dependencies.
No bad APIs such as association(). Better APIs, no strange factory API that introduces redundant concepts as traits,or implicit association behavior. Your model inserts are your factories. You can easily create different ES6 standardmethods on the model modules, thus memoria is easier and better to extend.
No implicit model lifecycle callbacks such as
beforeCreate
,afterCreate
,afterUpdate
,beforeDelete
etc.This is an old concept that is generally deemed harmful for development, we shouldn't do that extra computation duringruntime for all CRUD. Autogenerating things after a model gets created is an implicit thus bad behavior. Validationscould be done in future as types or TS type decorators(likeclass-validator
npm package).route shorthands accept the model definition to execute default behavior:
this.post('/users', User)
doesn't need to dasherize,underscore or do any other string manipulation to get the reference model definition. It also returns correct defaulthttp status code based on the HTTP verb, ex. HTTP POST returns 201 Created just like mirage.very easy to debug/develop the server, serialize any data in a very predictable and functional way.
API is very similar to Mirage, it can do everything mirage can do, while all redudant and worse API removed.
written in Typescript, thus provides type definitions by default.
About
Single JS/TS ORM for frontend, backend & in-memory in-browser testing. Based on typeorm API, allows you to change adapters: MemoryAdapter, RESTAdapter, SQLAdapter etc.