0.19.13 • Published 2 days ago

sealious v0.19.13

Weekly downloads
678
License
BSD-2-Clause
Repository
github
Last release
2 days ago

Sealious

Sealious is a declarative node.js framework. It creates a full-featured REST-ful API (with user and session management) based on a declarative description of the database schema and policies.

All development is handled on Sealcode's Phabricator. A read-only mirror is stored on Github.

Quick links

Examples

It's best to learn by example. Here are some applications written with the current version of Sealious:

  • Sealious Playground - simple TODO app written in Sealious and Hotwire. Contains docker setup for mongo, linting, typescript etc. Good starting point for a new app.

References

Example app

Install sealious with npm install --save sealious. Then, in your index.ts:

lang=typescript
import { resolve } from "path";
import Sealious, { App, Collection, FieldTypes, Policies } from "sealious";
const locreq = _locreq(__dirname);

const app = new (class extends App {
    config = {
        datastore_mongo: {
            host: "localhost",
            port: 20723,
            db_name: "sealious-playground",
        },
        upload_path: locreq.resolve("uploaded_files"),
        email: {
            from_address: "sealious-playground@example.com",
            from_name: "Sealious playground app",
        },
        "www-server": {
            port: 8080, //listen on this port
        },
    };
    manifest = {
        name: "My ToDo list",
        logo: resolve(__dirname, "../assets/logo.png"),
        version: "0.0.1",
        default_language: "en",
        base_url: "localhost:8080",
        admin_email: "admin@example.com",
        colors: {
            primary: "#5294a1",
        },
    };
    collections = {
        ...App.BaseCollections,
        tasks: new (class extends Collection {
            fields = {
                title: new FieldTypes.Text(),
                done: new FieldTypes.Boolean(),
            };
            defaultPolicy = new Policies.Public();
        })(),
    };
})();

app.start();

Assuming you have the mongo database running, that's it! The above script creates a fully functional REST API with field validation, error messages, etc. Try sending as POST message to http://localhost:8080/api/v1/collections/tasks to see the API in action. You can learn more about the endpoints created by Sealious for each collection in ./endpoints.remarkup doc file.

The app created by the above code also has some handy ORM-style methods to access and modify items within the collection:

lang=typescript
import {Context} from "sealious";

const tasks = app.collections.tasks.list(new Context(app)).fetch()

To learn more about the ORM methods, see ./orm.remarkup doc file.

Learning Resources

FAQ

How do I add a custom route?

Sealious uses koa and @koa/router to handle HTTP. To add a simple static route:

lang=typescript
app.HTTPServer.router.get("/", async (ctx) => {
    ctx.body = html(/* HTML */ `
        <body>
            <h1>Hello, world!</h1>
        </body>
    `);
});

If you need to perform some user-specific tasks, or need to extract the context in order to call the database, use the extractContext Middleware:

lang=typescript
import {Middlewares} from "sealious";

app.HTTPServer.router.get("/", Middlewares.extractContext(), async (ctx) => {
    const {items: tasks} = await app.collections.tasks.list(ctx.$context).fetch();
    ctx.body = html(/* HTML */ `
        <body>
            <h1>My To do list</h1>
            {tasks.map(task=>task.get("title")).join("")}
        </body>
    `);
});

How do I serve static files?

lang=typescript
app.HTTPServer.addStaticRoute("/", locreq.resolve("public"));

How do I set up SMTP?

When mailer isn't specified, Sealious log messages to stdout instead of sending them via email. To make it use an SMTP connection, add the following to the app definition:

lang=typescript
import { SMTPMailer } from "sealious";

// in app definition:

const app = new (class extends App {
    config = {
        /* ... */
    };
    mailer = new SMTPMailer({
        host: "localhost",
        port: 1025,
        user: "any",
        password: "any",
    });
})();

How do I change a policy for a built-in collection?

lang=typescript
const app = new (class extends App {
    config = {
        /* ... */
    };
    manifest = {
        /* ... */
    };
    collections = {
        ...App.BaseCollections,
        users: App.BaseCollections.users.setPolicy(
            "create",
            new Policies.Public()
        ),
    };
})();

How do I add a field to a built-in collection?

lang=typescript
import {Collections} from "sealious";

const app = new (class extends App {
    config = {
    /* ... */
    };
    manifest = {
    /* ... */
    };
    collections = {
        ...App.BaseCollections,

        users: new (class users extends Collections.users {
            fields = {
                ...App.BaseCollections.users.fields,
                description: new FieldTypes.Text(),
            };
        })(),
    };
})();

How to create a custom login endpoint?

lang=typescript
function LoginForm(username: string = "", error_message?: string) {
    return /* HTML */ `
        <form method="POST" action="/login">
            ${error_message ? `<div>${error_message}</div>` : ""}
            <label for="username">
                Username:
                <input
                    id="username"
                    name="username"
                    type="text"
                    value="${username}"
                    required
                />
            </label>
            <label for="password"
                >Password:
                <input
                    id="password"
                    name="password"
                    type="password"
                    value="${username}"
                    required
            /></label>
            <input type="submit" value="log in" />
        </form>
    `;
}

const router = app.HTTPServer.router;

router.get("/login", async (ctx) => {
    ctx.body = LoginForm();
});

router.post("/login", Middlewares.parseBody(), async (ctx) => {
    try {
        const session_id = await ctx.$app.collections.sessions.login(
            ctx.$body.username as string,
            ctx.$body.password as string
        );
        ctx.cookies.set("sealious-session", session_id, {
            maxAge: 1000 * 60 * 60 * 24 * 7,
            secure: ctx.request.protocol === "https",
            overwrite: true,
        });
        ctx.redirect("/user");
        ctx.status = 303; // more standards- and hotwire-friendly
    } catch (e) {
        ctx.body = LoginForm(ctx.$body.username as string, e.message);
    }
});

How to log out a user?

Create an endpoint where you call the sessions.logout function:

lang=ts
router.get("/logout", async (ctx) => {
	const session_id = ctx.cookies.get("sealious-session");
	ctx.$app.collections.sessions.logout(ctx.$context, session_id);
	ctx.status = 303; // more standards- and hotwire-friendly
});

How to set up a default value to a field?

It's possible, but currently not pretty. This will be fixed in the future.

lang=typescript
const tasks = new (class extends Collection {
	fields = {
		title: new FieldTypes.Text(),
		done: new (class extends FieldTypes.Boolean {
			hasDefaultValue = () => true;
			async getDefaultValue() {
				return false;
			}
		})(),
	};
	defaultPolicy = new Policies.Public();
})();

How to sort by modification/creation time?

lang=typescript
app.collections.entries
	.suList()
	.sort({ "_metadata.modified_at": "desc" }) // or: _metadata.created_at
	.fetch();

How to add custom validation to a collection?

lang=typescript
export class CollectionWithComplexValidation extends Collection {
  fields = {
    color: new FieldTypes.Color(),
  };

  async validate(_: Context, body: CollectionItemBody) {
    if ((body.getInput("color") as string).includes("green")) {
      return [{
          error: "Green is not a creative color",
          fields: ["color"],
       }];
    }
    return [];
  }

  defaultPolicy = new Policies.Public();
}

How to do one-time collection populate?

lang=typescript, name=collection.ts
const my_collection = new (class extends Collection {
  // ...
  async populate(): Promise<void> {
    if (await this.app.Metadata.get("my_collection_populated")) {
      return;
    }
    const app = this.app as TheApp;

	// create the resources here using the regular CRUD functions

    await this.app.Metadata.set("my_collection_populated", "true");
  }
})();
lang=typescript, name=index.ts
void app.start().then(async () => {
	await app.collections.my_collection.populate();
});

How to send an email?

lang=typescript
import { EmailTemplates } from "sealious";

const message = await EmailTemplates.Simple(ctx.$app, {
  text: "Click this link to finish registration:",
  subject: "Rejestracja w zmagazynu.pl",
  to: ctx.$body.email as string,
  buttons: [
    {
      text: "Finish registration",
      href: `${ctx.$app.manifest.base_url}/finish-registration?token=${some_token}`,
    },
  ],
});
message.send(ctx.$app);

How to translate strings returned by the app?

You can add custom translations to the app.strings object.

lang=typescript
const app = new (class extends App {
  // ...
  strings = {
    Welcome: "Witamy",
    you_have_n_points: (n: number) => `Masz ${n} punktów.`,
  };
})();

You can then use the translation anywhere in your app like so:

lang=typescript
app.getString("Welcome", [], "Welcome!");

Note: not all strings generated by Sealious are translatable yet.

How to run custom code when an item is created/edited?

When DerivedValue / CachedValue / ReverseSingleReference aren't enough for the logic of the application, you can use custom logic based on events.

lang=typescript

export default class Patrons extends Collection {
	fields = {
		fullname: new FieldTypes.Text(),
		amount_monthly: new FieldTypes.Float(),
		platform: new FieldTypes.Enum(["patronite", "liberapay", "manual"]),
	};
	defaultPolicy = new Policies.Public();

	async init(app: TheApp, collection_name: string) {
		await super.init(app, collection_name);
		this.on("after:create", async ([context, item, event]) => {
			await app.collections["patron-events"].create(context, {
				message: `Added a new patron from  ${item.get(
					"platform"
				)} for the amount: ${item.get("amount_monthly")}`,
				timestamp: Date.now(),
				patron: item.id,
			});
		});
	}
}

How to detect changes in particular fields?

When DerivedValue / CachedValue / ReverseSingleReference aren't enough for the logic of the application, you can use custom logic based on events.

lang=ts
export default class Patrons extends Collection {
	fields = {
		fullname: new FieldTypes.Text(),
		email: new FieldTypes.Email(),
		amount_monthly: new FieldTypes.Float(),
	};
	defaultPolicy = new Policies.Public();

	async init(app: TheApp, collection_name: string) {
		await super.init(app, collection_name);

		this.on("after:edit", async ([context, item]) => {
			const changes = await item.summarizeChanges(context);
			// when `amount_monthly` changes from 10 to 12, this will be `{amount_monthly: {was: 10, is: 12}}`
		});
	}
}

Development

To run test outside of docker, run:

docker-compose up -d
npm run test-cmd

If you want to debug the tests, run:

npm run test-cmd -- --debug
0.19.11

2 days ago

0.19.12

2 days ago

0.19.13

2 days ago

0.19.10

3 days ago

0.19.8

5 days ago

0.19.9

5 days ago

0.19.7

9 days ago

0.19.2

11 days ago

0.19.3

11 days ago

0.19.4

10 days ago

0.19.5

10 days ago

0.19.6

10 days ago

0.19.0

11 days ago

0.19.1

11 days ago

0.18.2

1 month ago

0.18.3

1 month ago

0.18.1

2 months ago

0.18.0

2 months ago

0.17.53

2 months ago

0.17.52

2 months ago

0.17.51

2 months ago

0.17.50

3 months ago

0.17.48

4 months ago

0.17.47

4 months ago

0.17.46

4 months ago

0.17.43

4 months ago

0.17.45

4 months ago

0.17.44

4 months ago

0.17.42

4 months ago

0.17.41

4 months ago

0.17.40

4 months ago

0.17.38

4 months ago

0.17.39

4 months ago

0.17.37

4 months ago

0.17.34

5 months ago

0.17.36

5 months ago

0.17.35

5 months ago

0.17.32

7 months ago

0.17.33

7 months ago

0.17.31

7 months ago

0.17.30

10 months ago

0.17.2

2 years ago

0.17.3

2 years ago

0.17.5

2 years ago

0.17.6

2 years ago

0.17.7

2 years ago

0.17.8

2 years ago

0.17.9

2 years ago

0.17.1

2 years ago

0.17.10

2 years ago

0.17.12

2 years ago

0.17.14

2 years ago

0.17.13

2 years ago

0.17.16

2 years ago

0.17.15

2 years ago

0.17.18

1 year ago

0.17.17

2 years ago

0.17.19

1 year ago

0.17.21

1 year ago

0.17.20

1 year ago

0.17.23

1 year ago

0.17.22

1 year ago

0.17.24

1 year ago

0.17.27

1 year ago

0.17.26

1 year ago

0.17.29

1 year ago

0.17.28

1 year ago

0.17.0

2 years ago

0.15.9

2 years ago

0.16.3

2 years ago

0.16.4

2 years ago

0.16.0

2 years ago

0.16.1

2 years ago

0.16.2

2 years ago

0.15.5

2 years ago

0.15.6

2 years ago

0.15.7

2 years ago

0.15.8

2 years ago

0.15.0

2 years ago

0.15.1

2 years ago

0.15.2

2 years ago

0.14.10

2 years ago

0.13.56

2 years ago

0.13.55

2 years ago

0.13.54

2 years ago

0.13.53

2 years ago

0.14.6

2 years ago

0.14.7

2 years ago

0.14.8

2 years ago

0.14.9

2 years ago

0.14.5

2 years ago

0.14.3

2 years ago

0.14.4

2 years ago

0.14.0--beta9

2 years ago

0.14.0--beta7

2 years ago

0.14.0--beta8

2 years ago

0.14.0--beta5

2 years ago

0.14.0--beta6

2 years ago

0.14.0--beta3

2 years ago

0.14.0--beta4

2 years ago

0.14.0--beta1

2 years ago

0.14.0--beta2

2 years ago

0.14.1

2 years ago

0.14.2

2 years ago

0.14.0--beta13

2 years ago

0.14.0--beta12

2 years ago

0.14.0--beta15

2 years ago

0.14.0--beta14

2 years ago

0.14.0--beta11

2 years ago

0.14.0--beta10

2 years ago

0.14.0--beta17

2 years ago

0.14.0--beta16

2 years ago

0.14.0--beta19

2 years ago

0.14.0--beta18

2 years ago

0.14.0--beta22

2 years ago

0.14.0--beta21

2 years ago

0.13.49

3 years ago

0.13.52

3 years ago

0.13.51

3 years ago

0.13.50

3 years ago

0.13.50-beta

3 years ago

0.13.50-beta3

3 years ago

0.13.50-beta2

3 years ago

0.13.48

3 years ago

0.13.47

3 years ago

0.13.46

3 years ago

0.13.45

3 years ago

0.13.44

3 years ago

0.13.43

3 years ago

0.13.42

3 years ago

0.13.41

3 years ago

0.13.40

3 years ago

0.13.39

3 years ago

0.13.38

3 years ago

0.13.37

3 years ago

0.13.36

3 years ago

0.13.34

3 years ago

0.13.35

3 years ago

0.13.32

3 years ago

0.13.31

3 years ago

0.13.29

3 years ago

0.13.30

3 years ago

0.13.27

3 years ago

0.13.28

3 years ago

0.13.26

3 years ago

0.13.25

3 years ago

0.13.23

3 years ago

0.13.24

3 years ago

0.13.22

3 years ago

0.13.21

3 years ago

0.13.20

3 years ago

0.13.19

3 years ago

0.13.18

3 years ago

0.13.17

3 years ago

0.13.16

3 years ago

0.13.15

3 years ago

0.13.14

3 years ago

0.13.13

3 years ago

0.13.12

3 years ago

0.13.11

3 years ago

0.13.10

3 years ago

0.13.9

3 years ago

0.13.8

3 years ago

0.13.7

3 years ago

0.13.6

3 years ago

0.13.5

3 years ago

0.13.4

3 years ago

0.13.3

3 years ago

0.13.2

3 years ago

0.13.1

3 years ago

0.13.0

3 years ago

0.12.6

3 years ago

0.12.4

3 years ago

0.12.5

3 years ago

0.12.3

3 years ago

0.12.2

3 years ago

0.12.1

3 years ago

0.12.0

3 years ago

0.10.15

4 years ago

0.10.14

4 years ago

0.10.13

4 years ago

0.10.11

4 years ago

0.10.12

4 years ago

0.10.10

4 years ago

0.10.9

4 years ago

0.10.8

4 years ago

0.10.7

4 years ago

0.10.6

4 years ago

0.10.5

4 years ago

0.10.4

4 years ago

0.10.3

4 years ago

0.10.1

4 years ago

0.10.0

4 years ago

0.9.5

5 years ago

0.9.4

5 years ago

0.6.24

8 years ago

0.6.23

8 years ago

0.6.22

8 years ago

0.6.21

8 years ago

0.6.20

8 years ago

0.6.19

8 years ago

0.6.18

8 years ago

0.7.11

8 years ago

0.7.10

8 years ago

0.6.17

8 years ago

0.7.9

8 years ago

0.7.8

8 years ago

0.7.7

8 years ago

0.7.6

8 years ago

0.7.5

8 years ago

0.7.4

9 years ago

0.7.3

9 years ago

0.6.16

9 years ago

0.6.15

9 years ago

0.6.14

9 years ago

0.6.13

9 years ago

0.6.12

9 years ago

0.6.11

9 years ago

0.6.10

9 years ago

0.6.9

9 years ago

0.6.8

9 years ago

0.6.7

9 years ago

0.6.6

9 years ago

0.6.5

9 years ago

0.6.4

9 years ago

0.6.3

9 years ago

0.6.2

9 years ago

0.6.1

9 years ago

0.6.0

9 years ago

0.5.13

9 years ago

0.5.12

9 years ago

0.5.11

9 years ago

0.5.10

9 years ago

0.5.9

9 years ago

0.5.8

9 years ago

0.5.7

9 years ago

0.5.6

9 years ago

0.5.5

9 years ago

0.5.4

9 years ago

0.5.3

9 years ago

0.5.2

9 years ago

0.5.1

9 years ago

0.5.0

9 years ago

0.4.9

9 years ago

0.4.7

9 years ago

0.4.6

9 years ago

0.4.4

9 years ago

0.4.2

9 years ago

0.4.1

9 years ago

0.4.0

9 years ago

0.3.9

9 years ago

0.3.6

9 years ago

0.3.5

9 years ago

0.3.4

9 years ago

0.3.3

9 years ago

0.3.2

9 years ago

0.3.1

9 years ago

0.3.0

9 years ago

0.2.13

9 years ago

0.2.11

9 years ago

0.2.10

9 years ago

0.2.8

9 years ago

0.2.7

9 years ago

0.2.5

9 years ago

0.2.4

9 years ago

0.2.3

9 years ago

0.2.2

9 years ago

0.2.1

9 years ago

0.2.0

9 years ago

0.1.23

9 years ago

0.1.21

9 years ago

0.1.20

9 years ago

0.1.19

9 years ago

0.1.18

9 years ago

0.1.17

9 years ago

0.1.15

9 years ago

0.1.14

9 years ago

0.1.13

9 years ago

0.1.12

9 years ago

0.1.11

9 years ago

0.1.10

9 years ago

0.1.9

9 years ago

0.1.8

9 years ago

0.1.7

9 years ago

0.1.6

9 years ago

0.1.5

9 years ago

0.1.4

9 years ago

0.1.3

9 years ago

0.1.2

9 years ago

0.1.1

9 years ago

0.0.14

9 years ago

0.0.13

9 years ago

0.0.12

9 years ago

0.0.11

9 years ago

0.0.10

9 years ago

0.0.9

9 years ago

0.0.8

9 years ago

0.0.7

9 years ago

0.0.6

9 years ago

0.0.5

9 years ago

0.0.4

9 years ago

0.0.3

9 years ago

0.0.2

9 years ago