michaellperry / jinaga

Universal web back-end, offering an application-agnostic API, real-time collaboration, and conflict resolution.
http://jinaga.com
MIT License
35 stars 3 forks source link

Jinaga is difficult to use with traditional workflows #68

Open saevarb opened 3 years ago

saevarb commented 3 years ago

Hi.

I was trying to go for a minimal example and did so by kind of jumping around in the docs on jinaga.com. I just wanted to construct a totally basic Jinaga server and then send stuff to it directly from node to begin with. I wrote a server.ts file and a client.ts file.

Unfortunately, it looks like JinagaBrowser uses XMLHttpRequest which causes a runtime failure when used on node since it's not available. Upon realizing this, I decided I would set up a quick create-react-app project and then use the client from there(I wanted to avoid starting an express server and manually dealing with webpack for the sole purpose of getting a client app up and running).

However, this quickly lead to issues with TypeScript: Importing import { JinagaBrowser } from "jinaga" works, but this seems to attempt to pull in pg-native, presumably because now I am also importing server code. I did however see your note about the client side code being in "jinaga/dist/jinaga", but TypeScript complains about this because there is no such module (according to the type definitions).

I know of libraries such as cross-fetch would could solve the problem with XMLHttpRequest, but I think it might be a good idea to consider some sort of monorepo workflow for code sharing, instead of users having to rely on bundlers.

michaellperry commented 3 years ago

Thanks for sticking with it and trying to resolve those problems. This is fantastic feedback that should help me resolve adoption issues.

The key that isn't well documented is that JinagaServer returns an object that has Jinaga initialized for server-side use. This instance does not use XHR, but instead uses Node's HTTP library.

export declare type JinagaServerInstance = {
    handler: Handler;
    j: Jinaga;
    withSession: (req: Request, callback: ((j: Jinaga) => Promise<void>)) => Promise<void>;
};

This is intended to be used from an Express app as follows:

const pgConnection = process.env.JINAGA_POSTGRESQL || 'postgresql://dev:devpw@localhost:5432/widgets';
const jinagaServer = JinagaServer.create({
    pgKeystore: pgConnection,
    pgStore: pgConnection,
    authorization: factAuthorization
});

const root = {
    type: "Widgets.Root",
    identifier: "root_identifier"
};

jinagaServer.j.fact(root)
    .catch(err => console.error(err));

app.use('/jinaga', jinagaServer.handler);

app.get("/api/widgets", (req, res) => {
    jinagaServer.withSession(req, async j => {
        const widgets = await j.query(root, j.for(widgetsInRoot));
        res
            .header('Content-Type', 'application/json')
            .send(JSON.stringify(widgets));
    });
});

The difference between the j and withSession fields is that withSession gives you back a Jinaga instance within the scope of the logged-in user.

The closest I have to this documentation is on the Jinaga Server page. It is incomplete and doesn't really explain what's going on.

And as you point out, it is specific to Express. I have not done anything to make it usable from create-react-app or any other server framework.

Some of the examples are written without the use of a bundler. But the documentation could be improved to make it clear that you have to be very careful not to reference jinaga/dist/jinaga from the server side or jinaga from the client.

Hope this helps in the short term. I will use this feedback to improve the documentation so that others can have a better experience.

michaellperry commented 3 years ago

I started a new set of "steps". Each is a small, focused set of instructions for solving a specific problem. The step Reference the Client Library is specifically indented to support those who want to use Jinaga in a more traditional workflow. It uses RequireJS instead of Webpack.

I'd like to research it a bit more and see if I can create a step for using create-react-app. The issue, though, is that the Jinaga client is of very limited value without the corresponding server middleware. And so once you learn how to use Jinaga on the client side, you will end up revamping your toolchain to bring in the server.

These steps try to make it clear that there are two bundles in the package: one for the server and one for the client. Please give it a review and let me know if this helps make the library easier to bring into your own preferred workflow.

saevarb commented 3 years ago

Hi Michael.

Thank you for your responses. I just tried revisiting this but I think I may not have explained my issues clearly, or I may be misunderstanding the architecture.

Here is how I would understand Jinaga and was expecting to be able to use it:

The latter step naturally requires either CORS or a proxy like nginx, but that is pretty par for the course.

Your documentation suggests that this is possible, but there are a couple of issues with this, at least in my view:

  1. It is very common that you don't have direct access to manipulate the webpack config(or similar), depending on what tooling you are using (such as create-react-app(CRA)).
  2. It is very common to deploy front-end applications separately as static assets and the backend elsewhere, and the documentation pretty much assumes that you are developing an express application which both serves your backend API as well as the frontend assets.

However, after playing around with it some more this time around, I have figured out that it is totally possible to use it with CRA, but there are issues with the typings, or perhaps the module structure(I'm not entirely sure).

import { JinagaBrowser } from "jinaga" fails on the front-end because it tries to import pg-native, which is expected because this is not the client side code. But import { JinagaBrowser } from "jinaga/dist/jinaga" fails because, according to typescript, that's not a valid module. It is possible to get it to work, but it requires you to abandon the types and do something like

const JinagaBrowser: any = require("jinaga/dist/jinaga").JinagaBrowser;
const j = JinagaBrowser.create({ httpEndpoint: "/jinaga" }); // for this to work in CRA, setting the "proxy" in package.json is required

j.fact({
  type: "Foo",
  bar: "Baz",
});

So the default CRA webpack configuration is capable of tree-shaking the unused pg-native module, but because the typings are off, I cannot import it properly without going this circuitous route. I played around with import type .. to be able to at least get correct typings, but this turned out to be less than straight-forward because the JinagaBrowser's create method is static. Regardless, it seems like there might be a relatively easy fix here, that might only involve something to do with the way the types are generated, to make this work as I think most people would expect it to.

However -- the problems I am running into(trying to use the JinagaBrowser from the frontend) are kind of problems that I created for myself just for the sake of testing, since it strikes me that it may not be desirable in all cases to have full access to the Jinaga client from the frontend, since then you cannot put any restrictions on the shape of your data. That is to say: I probably want something more like your /api/widgets approach anyway, in which case I don't need to directly import Jinaga at all. OTOH, that also limits flexibility and maybe makes synchronization more complicated?

I ended up going back and restructuring this message a few times and it seems kind of rambly and incoherent to me right now, so don't hesitate to ask if you need me to clarify anything.

michaellperry commented 3 years ago

That makes a lot of sense. I haven't tried it in Create React App. That's definitely a good argument for splitting the client and server-side packages into two. I think that's the simplest solution to this problem.