@sealcode/sealgen 0.15.23 → 0.15.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/@types/src/add-collection.d.ts +1 -1
  2. package/@types/src/page/list.d.ts +1 -0
  3. package/@types/src/page/sealious-list.d.ts +3 -2
  4. package/@types/src/page/sealious-list.test.d.ts +1 -0
  5. package/@types/src/test_utils/exec_async.d.ts +1 -0
  6. package/@types/src/test_utils/test-on-real-app.d.ts +24 -0
  7. package/@types/src/utils/input-wrapper.d.ts +1 -1
  8. package/lib/add-collection.js +18 -8
  9. package/lib/add-route.test.js +9 -92
  10. package/lib/forms/controls/checkboxed-list-input.js +31 -16
  11. package/lib/forms/controls/multiple-files.js +1 -0
  12. package/lib/forms/controls/simple-input.js +1 -0
  13. package/lib/forms/controls/tickable.js +1 -0
  14. package/lib/page/list.js +1 -0
  15. package/lib/page/sealious-list.js +9 -2
  16. package/lib/page/sealious-list.test.js +24 -0
  17. package/lib/templates/list.js +2 -1
  18. package/lib/test_utils/exec_async.js +24 -0
  19. package/lib/test_utils/test-on-real-app.js +147 -0
  20. package/lib/utils/input-wrapper.js +4 -2
  21. package/package.json +1 -1
  22. package/src/add-collection.ts +23 -8
  23. package/src/add-route.test.ts +9 -99
  24. package/src/forms/controls/checkboxed-list-input.ts +40 -25
  25. package/src/forms/controls/multiple-files.ts +1 -0
  26. package/src/forms/controls/simple-input.ts +1 -0
  27. package/src/forms/controls/tickable.ts +1 -0
  28. package/src/page/list.ts +1 -1
  29. package/src/page/sealious-list.test.ts +25 -0
  30. package/src/page/sealious-list.ts +12 -4
  31. package/src/templates/list.ts +2 -1
  32. package/src/test_utils/exec_async.ts +26 -0
  33. package/src/test_utils/test-on-real-app.ts +167 -0
  34. package/src/utils/input-wrapper.ts +4 -1
@@ -1 +1 @@
1
- export declare function addCollection(): Promise<void>;
1
+ export declare function addCollection(params?: Record<string, string | boolean>): Promise<void>;
@@ -9,6 +9,7 @@ import { SimpleFormField } from "../forms/fields/simple-form-field.js";
9
9
  import { FormDataValue } from "../forms/form-types.js";
10
10
  export declare const BasePagePropsShape: {};
11
11
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
12
+ export declare const DEFAULT_ITEMS_PER_PAGE = 12;
12
13
  export declare const BaseListPageFields: {
13
14
  readonly page: NumberField<1, false>;
14
15
  readonly itemsPerPage: NumberField<12, false>;
@@ -1,7 +1,7 @@
1
1
  import { Context } from "koa";
2
2
  import { BaseListPageFields, ListPage } from "./list.js";
3
3
  import { Collection, CollectionItem, ItemList, Context as SealiousContext } from "sealious";
4
- import { FlatTemplatable } from "tempstream";
4
+ import { FlatTemplatable, Templatable } from "tempstream";
5
5
  export declare abstract class SealiousItemListPage<C extends Collection, F extends typeof BaseListPageFields> extends ListPage<CollectionItem<C>, F> {
6
6
  collection: C;
7
7
  constructor(collection: C);
@@ -9,8 +9,9 @@ export declare abstract class SealiousItemListPage<C extends Collection, F exten
9
9
  getTotalPages(ctx: Context, itemsPerPage: number): Promise<number>;
10
10
  protected addFilter(ctx: Context, list: ItemList<C>): Promise<void>;
11
11
  makeQuery(ctx: Context): Promise<ItemList<C>>;
12
- getItems(ctx: Context, page: number, itemsPerPage: number): Promise<CollectionItem<C>[]>;
12
+ getItems(ctx: Context, page: number, itemsPerPage: number | null): Promise<CollectionItem<C>[]>;
13
13
  getFilterValues(ctx: Context): Promise<Record<string, string | undefined>>;
14
+ renderListContainer(_: Context, content: Templatable): FlatTemplatable;
14
15
  renderItem(_: Context, item: CollectionItem<C>, _index: number): Promise<FlatTemplatable>;
15
16
  canAccess: (ctx: Context) => Promise<{
16
17
  canAccess: boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const execAsync: (cmd: string) => Promise<void>;
@@ -0,0 +1,24 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ import EventEmitter from "events";
3
+ export declare const callSealgenBin: () => string;
4
+ export declare class RealAppTest {
5
+ node_pid: number;
6
+ app_port: number;
7
+ mongo_port: number;
8
+ app_path: string;
9
+ container_name: string;
10
+ emitter: EventEmitter;
11
+ static init(): Promise<RealAppTest>;
12
+ start(): Promise<void>;
13
+ close(): Promise<void>;
14
+ runSealgenCommand(command: string): Promise<void>;
15
+ throwIfItKillsTheApp<T>(cb: () => Promise<T>): Promise<T>;
16
+ httpGet(path: string): Promise<{
17
+ status: number;
18
+ response: string;
19
+ }>;
20
+ httpPOST(path: string, body: Record<string, unknown>): Promise<{
21
+ status: number;
22
+ response: string;
23
+ }>;
24
+ }
@@ -2,4 +2,4 @@
2
2
  import { Readable } from "stream";
3
3
  import { FlatTemplatable } from "tempstream";
4
4
  export declare function getRequiredClass(isRequired: boolean): string;
5
- export declare function inputWrapper(modifiers: string[], input: FlatTemplatable): Readable;
5
+ export declare function inputWrapper(type: string, modifiers: string[], input: FlatTemplatable): Readable;
@@ -1,18 +1,28 @@
1
1
  import { resolve } from "path";
2
2
  import _locreq from "locreq";
3
3
  import { promises as fs, existsSync } from "fs";
4
- import { question } from "./utils/question.js";
5
4
  import { collectionTemplate } from "./templates/collection.js";
6
5
  import { generateCollections } from "./generate-collections.js";
7
6
  import { toKebabCase, toPascalCase } from "js-convert-case";
8
7
  const target_locreq = _locreq(process.cwd());
9
- async function addCollection() {
10
- const collection_name = toPascalCase(
11
- await question(
12
- "What's the name of the collection class: ",
13
- (s) => s.length > 3
14
- )
15
- );
8
+ import prompts from "prompts";
9
+ async function addCollection(params = {}) {
10
+ prompts.override(params);
11
+ const response = await prompts([
12
+ {
13
+ type: "text",
14
+ name: "collection_name",
15
+ message: "What's the name of the collection class: ",
16
+ validate: (s) => s.length > 3 ? true : "Should be at least 3 characters long",
17
+ format: function(action) {
18
+ return toPascalCase(action);
19
+ }
20
+ }
21
+ ]);
22
+ const collection_name = response.collection_name;
23
+ if (!collection_name) {
24
+ throw new Error("Empty collection name, please try again");
25
+ }
16
26
  const file_path = target_locreq.resolve(
17
27
  `src/back/collections/${toKebabCase(collection_name)}.ts`
18
28
  );
@@ -1,98 +1,15 @@
1
- import { promises as fs } from "fs";
2
- import { exec, spawn } from "child_process";
3
- import axios from "axios";
4
- import waitPort from "wait-port";
5
- import getPort from "get-port";
6
- import _locreq from "locreq";
7
- import { module_dirname } from "./utils/module-dirname.js";
8
- const locreq = _locreq(module_dirname(import.meta.url));
9
- const execAsync = (cmd) => {
10
- return new Promise((resolve, reject) => {
11
- const process2 = exec(cmd);
12
- console.log("EXECUTING", cmd);
13
- process2?.stdout?.on("data", (data) => {
14
- console.log(`stdout: ${data}`);
15
- });
16
- process2?.stderr?.on("data", (data) => {
17
- console.log(`stdout: ${data}`);
18
- });
19
- process2.on("exit", (code) => {
20
- if (code == 0) {
21
- resolve();
22
- } else {
23
- reject();
24
- }
25
- });
26
- });
27
- };
28
- const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
1
+ import { RealAppTest } from "./test_utils/test-on-real-app.js";
29
2
  describe("add route test", () => {
30
- let node_pid = 0;
31
- const timestamp = Date.now();
32
- const app_path = `/tmp/sealious-minimal-${timestamp}`;
33
- const container_name = `sealgen-test-mongo-${timestamp}`;
34
- afterEach(async function() {
35
- try {
36
- if (node_pid != 0)
37
- await execAsync(`kill -9 ${node_pid}`);
38
- } catch (error) {
39
- throw new Error("node process exited unexpectedly, please inspect the output");
40
- }
41
- await Promise.all([
42
- fs.rm(app_path, { recursive: true, force: true }),
43
- execAsync(`docker rm -f ${container_name}`)
44
- ]);
45
- });
46
- it("creates minimal sealious app", async function() {
47
- console.log(" cloning sealious-minimal...");
48
- await execAsync(
49
- `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout df8cccf`
50
- );
51
- console.log(" installing dependencies and sealgen...");
52
- await execAsync(
53
- `cd ${app_path} && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
54
- );
55
- console.log(" running add-route and building app...");
56
- await execAsync(
57
- `cd ${app_path} && ${callSealgenBin()} add-route --url=/sample --action=Sample --mode=page && ${callSealgenBin()} build`
58
- );
59
- console.log(
60
- " starting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
61
- );
62
- const mongo_port = await getPort();
63
- const app_port = await getPort();
64
- await execAsync(
65
- `docker run -p ${mongo_port}:27017 --name ${container_name} -d mongo:4.4-bionic`
66
- );
67
- console.log(" running app and making request to the generated url...");
68
- const env = {
69
- ...process.env,
70
- SEALIOUS_MONGO_PORT: mongo_port.toString(),
71
- SEALIOUS_PORT: app_port.toString()
72
- };
73
- const node_args = [`${app_path}/dist/back/index.js`];
74
- console.log(
75
- "EXECUTING APP",
76
- `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join(" ")} node ${node_args.join(" ")}`
77
- );
78
- let app_start = spawn(`node`, node_args, {
79
- env
80
- });
81
- app_start.stdout.on("data", (data) => console.log("APP OUTPUT:", data.toString()));
82
- app_start.stderr.on("data", (data) => console.log(data.toString()));
83
- if (app_start.pid == void 0)
84
- throw new Error("node failed to start");
85
- node_pid = app_start.pid;
86
- await waitPort({
87
- host: "localhost",
88
- port: app_port,
89
- output: "silent",
90
- timeout: 6e3
91
- });
92
- const res = await axios.get(`http://localhost:${app_port}/sample`);
3
+ it("creates minimal sealious app and adds a simple route that works", async function() {
4
+ const test_app = await RealAppTest.init();
5
+ const path = "/sample";
6
+ await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
7
+ await test_app.start();
8
+ const res = await test_app.httpGet(path);
93
9
  if (res.status != 200) {
94
- throw new Error("url generated by add-route exists but doesnt return 200");
10
+ throw new Error("url generated by add-route exists but doesn't return 200");
95
11
  }
12
+ await test_app.close();
96
13
  }).timeout(100 * 1e3);
97
14
  });
98
15
  //# sourceMappingURL=add-route.test.js.map
@@ -20,28 +20,43 @@ class CheckboxedListInput extends FormFieldControl {
20
20
  ]);
21
21
  const { name, required } = this.field;
22
22
  return inputWrapper(
23
- [name, getRequiredClass(required)],
23
+ "checkboxed-list",
24
+ [
25
+ name,
26
+ getRequiredClass(required),
27
+ "options-count--" + options.length.toString(),
28
+ ...options.length >= 5 ? ["options-count--5-or-more"] : [],
29
+ ...options.length >= 10 ? ["options-count--10-or-more"] : [],
30
+ ...options.length >= 15 ? ["options-count--15-or-more"] : [],
31
+ ...options.length >= 20 ? ["options-count--20-or-more"] : []
32
+ ],
24
33
  tempstream`<label
25
34
  >${this.options.label || this.field.name}</label
26
- >${isVisible ? options.map(
27
- ({ value, label }) => inputWrapper(
35
+ >
36
+ <ul>
37
+ ${isVisible ? options.map(
38
+ ({ value, label }) => tempstream`<li>
39
+ ${inputWrapper(
40
+ "checkbox",
28
41
  [name],
29
42
  /* HTML */
30
43
  `
31
- <input
32
- type="checkbox"
33
- id="${name}.${value}"
34
- name="${name}.${value}"
35
- ${pickedValues.includes(value) ? "checked" : ""}
36
- form=${fctx.form_id}
37
- />
38
- <label for="${name}.${value}"
39
- >${label || value}</label
40
- >
41
- `
42
- )
44
+ <input
45
+ type="checkbox"
46
+ id="${name}.${value}"
47
+ name="${name}.${value}"
48
+ ${pickedValues.includes(value) ? "checked" : ""}
49
+ form=${fctx.form_id}
50
+ />
51
+ <label for="${name}.${value}"
52
+ >${label || value}</label
53
+ >
54
+ `
55
+ )}
56
+ </li>`
43
57
  ) : ""}
44
- ${~valid && fctx.validate ? `<div class="input__error">${message}</div>` : ""}`
58
+ ${~valid && fctx.validate ? `<div class="input__error">${message}</div>` : ""}
59
+ </ul>`
45
60
  );
46
61
  }
47
62
  }
@@ -161,6 +161,7 @@ class MultipleFiles extends FormFieldControl {
161
161
  ])
162
162
  )).filter(([_, f]) => f !== null);
163
163
  return inputWrapper(
164
+ "multiple-files",
164
165
  ["multiple-files", this.field.name, ...this.getClassModifiers()],
165
166
  tempstream`
166
167
  <label>${this.options.label || this.field.name}</label>
@@ -73,6 +73,7 @@ class SimpleInput extends FormFieldControl {
73
73
  true
74
74
  );
75
75
  return inputWrapper(
76
+ "simple-input",
76
77
  this.getWrapperModifiers(),
77
78
  tempstream`
78
79
  ${label ? `<label for="${id}">${label}</label>` : ""}
@@ -33,6 +33,7 @@ class Tickable extends FormFieldControl {
33
33
  const readonly = this.options.readonly || false;
34
34
  const required = this.field.required;
35
35
  return inputWrapper(
36
+ this.type,
36
37
  this.getWrapperClasses(fctx.ctx, fctx.data, parsed),
37
38
  /* HTML */
38
39
  `
package/lib/page/list.js CHANGED
@@ -202,6 +202,7 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
202
202
  export {
203
203
  BaseListPageFields,
204
204
  BasePagePropsShape,
205
+ DEFAULT_ITEMS_PER_PAGE,
205
206
  ListPage
206
207
  };
207
208
  //# sourceMappingURL=list.js.map
@@ -1,4 +1,8 @@
1
- import { ListPage } from "./list.js";
1
+ import {
2
+ DEFAULT_ITEMS_PER_PAGE,
3
+ ListPage
4
+ } from "./list.js";
5
+ import { tempstream } from "tempstream";
2
6
  import { peopleWhoCan } from "./access-control.js";
3
7
  import qs from "qs";
4
8
  import { is, predicates } from "@sealcode/ts-predicates";
@@ -39,7 +43,7 @@ class SealiousItemListPage extends ListPage {
39
43
  async getItems(ctx, page, itemsPerPage) {
40
44
  const query = await this.makeQuery(ctx);
41
45
  await this.addFilter(ctx, query);
42
- const { items } = await query.paginate({ items: itemsPerPage, page }).fetch();
46
+ const { items } = await query.paginate({ items: itemsPerPage || DEFAULT_ITEMS_PER_PAGE, page }).fetch();
43
47
  return items;
44
48
  }
45
49
  async getFilterValues(ctx) {
@@ -51,6 +55,9 @@ class SealiousItemListPage extends ListPage {
51
55
  ) ? _filter_values : {};
52
56
  return filter_values;
53
57
  }
58
+ renderListContainer(_, content) {
59
+ return tempstream`<div class="sealgen-sealious-list-container">${content}</div>`;
60
+ }
54
61
  async renderItem(_, item, _index) {
55
62
  return `<div>${item.id}</div>`;
56
63
  }
@@ -0,0 +1,24 @@
1
+ import assert from "assert";
2
+ import { RealAppTest } from "../test_utils/test-on-real-app.js";
3
+ describe("sealious-list test", () => {
4
+ it("creates minimal sealious app and adds a simple route that works", async function() {
5
+ const test_app = await RealAppTest.init();
6
+ const url = "/list";
7
+ await test_app.runSealgenCommand(`add-collection --collection_name=samples`);
8
+ await test_app.runSealgenCommand(
9
+ `add-route --url=${url} --action=List --mode=list --collection=samples`
10
+ );
11
+ await test_app.start();
12
+ const res_post = await test_app.httpPOST("/api/v1/collections/samples", {
13
+ content: "some-content"
14
+ });
15
+ assert(res_post.status == 201);
16
+ const res = await test_app.httpGet(url);
17
+ if (res.status != 200) {
18
+ throw new Error("url generated by add-route exists but doesn't return 200");
19
+ }
20
+ assert(res.response.includes("some-content"));
21
+ await test_app.close();
22
+ }).timeout(100 * 1e3);
23
+ });
24
+ //# sourceMappingURL=sealious-list.test.js.map
@@ -6,6 +6,7 @@ import { extractCollectionClassname } from "../generate-collections.js";
6
6
  import extract_fields_from_collection from "../utils/extract-fields-from-collection.js";
7
7
  import { DefaultListFilters } from "../page/default-list-filters.js";
8
8
  import { formatWithPrettier } from "../utils/prettier.js";
9
+ import { toKebabCase } from "js-convert-case";
9
10
  const target_locreq = _locreq(process.cwd());
10
11
  const listTemplate = async (action_name, newfilefullpath) => {
11
12
  const response = await prompts({
@@ -147,7 +148,7 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
147
148
  return html(
148
149
  ctx,
149
150
  "${action_name}",
150
- <div>
151
+ <div class="sealious-list-wrapper ${toKebabCase(action_name)}--wrapper">
151
152
  <h2>${action_name} List</h2>
152
153
  <table class="sealious-list">
153
154
  {this.renderTableHead(ctx, displayFields)}
@@ -0,0 +1,24 @@
1
+ import { exec } from "child_process";
2
+ const execAsync = (cmd) => {
3
+ return new Promise((resolve, reject) => {
4
+ const process = exec(cmd);
5
+ console.log("EXECUTING", cmd);
6
+ process?.stdout?.on("data", (data) => {
7
+ console.log(`stdout: ${data}`);
8
+ });
9
+ process?.stderr?.on("data", (data) => {
10
+ console.log(`stdout: ${data}`);
11
+ });
12
+ process.on("exit", (code) => {
13
+ if (code == 0) {
14
+ resolve();
15
+ } else {
16
+ reject();
17
+ }
18
+ });
19
+ });
20
+ };
21
+ export {
22
+ execAsync
23
+ };
24
+ //# sourceMappingURL=exec_async.js.map
@@ -0,0 +1,147 @@
1
+ import { execAsync } from "./exec_async.js";
2
+ import { spawn } from "child_process";
3
+ import getPort from "get-port";
4
+ import waitPort from "wait-port";
5
+ import { promises as fs } from "fs";
6
+ import _locreq from "locreq";
7
+ import { module_dirname } from "../utils/module-dirname.js";
8
+ import EventEmitter from "events";
9
+ const locreq = _locreq(module_dirname(import.meta.url));
10
+ const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
11
+ const sealious_minimal_commit = "b0d5bf5";
12
+ class RealAppTest {
13
+ constructor() {
14
+ this.emitter = new EventEmitter();
15
+ }
16
+ static async init() {
17
+ const test_app = new RealAppTest();
18
+ test_app.node_pid = 0;
19
+ const timestamp = Date.now();
20
+ test_app.app_path = `/tmp/sealious-minimal-${timestamp}`;
21
+ test_app.container_name = `sealgen-test-mongo-${timestamp}`;
22
+ console.log(" cloning sealious-minimal...");
23
+ await execAsync(
24
+ `git clone https://hub.sealcode.org/source/sealious-minimal.git ${test_app.app_path} && cd ${test_app.app_path} && git checkout ${sealious_minimal_commit}`
25
+ );
26
+ console.log(" installing dependencies and sealgen...");
27
+ await execAsync(
28
+ `cd ${test_app.app_path} && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
29
+ );
30
+ console.log(
31
+ " starting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
32
+ );
33
+ test_app.app_port = await getPort();
34
+ return test_app;
35
+ }
36
+ async start() {
37
+ this.mongo_port = await getPort();
38
+ await execAsync(
39
+ `docker run -p ${this.mongo_port}:27017 --name ${this.container_name} -d mongo:4.4-bionic`
40
+ );
41
+ const env = {
42
+ ...process.env,
43
+ SEALIOUS_MONGO_PORT: this.mongo_port.toString(),
44
+ SEALIOUS_PORT: this.app_port.toString()
45
+ };
46
+ const node_args = [`${this.app_path}/dist/back/index.js`];
47
+ console.log(
48
+ "EXECUTING APP",
49
+ `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join(" ")} node ${node_args.join(" ")}`
50
+ );
51
+ const app_start = spawn(`node`, node_args, {
52
+ env
53
+ });
54
+ app_start.stdout.on(
55
+ "data",
56
+ (data) => console.log("APP OUTPUT:", data.toString())
57
+ );
58
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
59
+ app_start.on("exit", (code) => {
60
+ this.emitter.emit("exit", code);
61
+ this.emitter.emit("process_error");
62
+ });
63
+ if (app_start.pid == void 0)
64
+ throw new Error("node failed to start");
65
+ this.node_pid = app_start.pid;
66
+ await waitPort({
67
+ host: "localhost",
68
+ port: this.app_port,
69
+ output: "silent",
70
+ timeout: 6e3
71
+ });
72
+ }
73
+ async close() {
74
+ try {
75
+ if (this.node_pid != 0)
76
+ await execAsync(`kill -9 ${this.node_pid}`);
77
+ } catch (error) {
78
+ throw new Error(
79
+ "node process exited unexpectedly, please inspect the output"
80
+ );
81
+ }
82
+ await Promise.all([
83
+ fs.rm(this.app_path, { recursive: true, force: true }),
84
+ execAsync(`docker rm -f ${this.container_name}`)
85
+ ]);
86
+ }
87
+ async runSealgenCommand(command) {
88
+ await execAsync(
89
+ `cd ${this.app_path} && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
90
+ );
91
+ }
92
+ async throwIfItKillsTheApp(cb) {
93
+ let finished = false;
94
+ return new Promise(async (resolve, reject) => {
95
+ const error_handler = () => {
96
+ if (!finished) {
97
+ reject();
98
+ }
99
+ finished = true;
100
+ };
101
+ this.emitter.addListener("process_error", error_handler);
102
+ try {
103
+ const result = await cb();
104
+ if (!finished) {
105
+ resolve(result);
106
+ finished = true;
107
+ }
108
+ } catch (e) {
109
+ if (!finished) {
110
+ reject(e);
111
+ finished = true;
112
+ }
113
+ }
114
+ this.emitter.removeListener("process_error", error_handler);
115
+ });
116
+ }
117
+ async httpGet(path) {
118
+ return this.throwIfItKillsTheApp(async () => {
119
+ const ret = await fetch(`http://localhost:${this.app_port}${path}`);
120
+ const status = ret.status;
121
+ const response = await ret.text();
122
+ return { status, response };
123
+ });
124
+ }
125
+ async httpPOST(path, body) {
126
+ return this.throwIfItKillsTheApp(async () => {
127
+ const ret = await fetch(
128
+ `http://localhost:${this.app_port}${path}`,
129
+ {
130
+ method: "POST",
131
+ headers: { "Content-Type": "application/json" },
132
+ body: JSON.stringify(body)
133
+ }
134
+ );
135
+ const status = ret.status;
136
+ const response = await ret.text();
137
+ console.log("request done!");
138
+ console.log({ response });
139
+ return { status, response };
140
+ });
141
+ }
142
+ }
143
+ export {
144
+ RealAppTest,
145
+ callSealgenBin
146
+ };
147
+ //# sourceMappingURL=test-on-real-app.js.map
@@ -5,9 +5,11 @@ function getRequiredClass(isRequired) {
5
5
  function getModifierClasses(modifiers) {
6
6
  return modifiers.map((m) => "form-input__wrapper--" + m).join(" ");
7
7
  }
8
- function inputWrapper(modifiers, input) {
8
+ function inputWrapper(type, modifiers, input) {
9
9
  return tempstream`<div
10
- class="form-input form-input__wrapper ${getModifierClasses(modifiers)}"
10
+ class="form-input form-input--type-${type} form-input__wrapper ${getModifierClasses(
11
+ modifiers
12
+ )}"
11
13
  >
12
14
  ${input}
13
15
  </div>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.15.23",
3
+ "version": "0.15.25",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -1,20 +1,35 @@
1
1
  import { resolve } from "path";
2
2
  import _locreq from "locreq";
3
3
  import { promises as fs, existsSync } from "fs";
4
- import { question } from "./utils/question.js";
5
4
  import { collectionTemplate } from "./templates/collection.js";
6
5
  import { generateCollections } from "./generate-collections.js";
7
6
  import { toKebabCase, toPascalCase } from "js-convert-case";
8
7
  const target_locreq = _locreq(process.cwd());
8
+ import prompts from "prompts";
9
9
 
10
- export async function addCollection(): Promise<void> {
11
- const collection_name = toPascalCase(
12
- await question(
13
- "What's the name of the collection class: ",
14
- (s: string) => s.length > 3
15
- )
16
- );
10
+ export async function addCollection(
11
+ params: Record<string, string | boolean> = {}
12
+ ): Promise<void> {
13
+ prompts.override(params);
14
+ const response = await prompts([
15
+ {
16
+ type: "text",
17
+ name: "collection_name",
18
+ message: "What's the name of the collection class: ",
19
+ validate: (s: string) =>
20
+ s.length > 3 ? true : "Should be at least 3 characters long",
21
+ format: function (action: string) {
22
+ return toPascalCase(action);
23
+ },
24
+ },
25
+ ]);
26
+
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28
+ const collection_name: string = response.collection_name;
17
29
 
30
+ if (!collection_name) {
31
+ throw new Error("Empty collection name, please try again");
32
+ }
18
33
  const file_path = target_locreq.resolve(
19
34
  `src/back/collections/${toKebabCase(collection_name)}.ts`
20
35
  );
@@ -1,106 +1,16 @@
1
- import { promises as fs } from "fs";
2
- import { exec, spawn } from "child_process";
3
- import axios from "axios";
4
- import waitPort from "wait-port";
5
- import getPort from "get-port";
6
-
7
- import _locreq from "locreq";
8
- import { module_dirname } from "./utils/module-dirname.js";
9
- const locreq = _locreq(module_dirname(import.meta.url));
10
-
11
- const execAsync = (cmd: string) => {
12
- return new Promise<void>((resolve, reject) => {
13
- const process = exec(cmd);
14
-
15
- console.log("EXECUTING", cmd);
16
-
17
- process?.stdout?.on("data", (data) => {
18
- console.log(`stdout: ${data}`);
19
- });
20
-
21
- process?.stderr?.on("data", (data) => {
22
- console.log(`stdout: ${data}`);
23
- });
24
-
25
- process.on("exit", (code) => {
26
- if (code == 0) {
27
- resolve();
28
- } else {
29
- reject();
30
- }
31
- });
32
- });
33
- };
34
-
35
- const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
1
+ import { RealAppTest } from "./test_utils/test-on-real-app.js";
36
2
 
37
3
  describe("add route test", () => {
38
- let node_pid = 0;
39
- const timestamp = Date.now();
40
- const app_path = `/tmp/sealious-minimal-${timestamp}`;
41
- const container_name = `sealgen-test-mongo-${timestamp}`;
42
- afterEach(async function () {
43
- try {
44
- if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
45
- } catch (error) {
46
- throw new Error("node process exited unexpectedly, please inspect the output");
47
- }
48
- await Promise.all([
49
- fs.rm(app_path, { recursive: true, force: true }),
50
- execAsync(`docker rm -f ${container_name}`),
51
- ]);
52
- });
4
+ it("creates minimal sealious app and adds a simple route that works", async function () {
5
+ const test_app = await RealAppTest.init();
53
6
 
54
- it("creates minimal sealious app", async function () {
55
- console.log("\tcloning sealious-minimal...");
56
- await execAsync(
57
- `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout df8cccf`
58
- );
59
- console.log("\tinstalling dependencies and sealgen...");
60
- await execAsync(
61
- `cd ${app_path} && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
62
- );
63
- console.log("\trunning add-route and building app...");
64
- await execAsync(
65
- `cd ${app_path} && ${callSealgenBin()} add-route --url=/sample --action=Sample --mode=page && ${callSealgenBin()} build`
66
- );
67
- console.log(
68
- "\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
69
- );
70
- const mongo_port = await getPort();
71
- const app_port = await getPort();
72
- await execAsync(
73
- `docker run -p ${mongo_port}:27017 --name ${container_name} -d mongo:4.4-bionic`
74
- );
75
- console.log("\trunning app and making request to the generated url...");
76
- const env = {
77
- ...process.env,
78
- SEALIOUS_MONGO_PORT: mongo_port.toString(),
79
- SEALIOUS_PORT: app_port.toString(),
80
- };
81
- const node_args = [`${app_path}/dist/back/index.js`];
82
- console.log(
83
- "EXECUTING APP",
84
- `${Object.entries(env)
85
- .map(([key, value]) => `${key}=${value}`)
86
- .join(" ")} node ${node_args.join(" ")}`
87
- );
88
- let app_start = spawn(`node`, node_args, {
89
- env,
90
- });
91
- app_start.stdout.on("data", (data) => console.log("APP OUTPUT:", data.toString()));
92
- app_start.stderr.on("data", (data) => console.log(data.toString()));
93
- if (app_start.pid == undefined) throw new Error("node failed to start");
94
- node_pid = app_start.pid;
95
- await waitPort({
96
- host: "localhost",
97
- port: app_port,
98
- output: "silent",
99
- timeout: 6000,
100
- });
101
- const res = await axios.get(`http://localhost:${app_port}/sample`);
7
+ const path = "/sample";
8
+ await test_app.runSealgenCommand(`add-route --url=${path} --action=Sample --mode=page`);
9
+ await test_app.start();
10
+ const res = await test_app.httpGet(path);
102
11
  if (res.status != 200) {
103
- throw new Error("url generated by add-route exists but doesnt return 200");
12
+ throw new Error("url generated by add-route exists but doesn't return 200");
104
13
  }
14
+ await test_app.close();
105
15
  }).timeout(100 * 1000);
106
16
  });
@@ -28,33 +28,48 @@ export class CheckboxedListInput extends FormFieldControl {
28
28
 
29
29
  const { name, required } = this.field;
30
30
  return inputWrapper(
31
- [name, getRequiredClass(required)],
31
+ "checkboxed-list",
32
+ [
33
+ name,
34
+ getRequiredClass(required),
35
+ "options-count--" + options.length.toString(),
36
+ ...(options.length >= 5 ? ["options-count--5-or-more"] : []),
37
+ ...(options.length >= 10 ? ["options-count--10-or-more"] : []),
38
+ ...(options.length >= 15 ? ["options-count--15-or-more"] : []),
39
+ ...(options.length >= 20 ? ["options-count--20-or-more"] : []),
40
+ ],
32
41
  tempstream/* HTML */ `<label
33
42
  >${this.options.label || this.field.name}</label
34
- >${isVisible
35
- ? options.map(({ value, label }) =>
36
- inputWrapper(
37
- [name],
38
- /* HTML */ `
39
- <input
40
- type="checkbox"
41
- id="${name}.${value}"
42
- name="${name}.${value}"
43
- ${pickedValues.includes(value)
44
- ? "checked"
45
- : ""}
46
- form=${fctx.form_id}
47
- />
48
- <label for="${name}.${value}"
49
- >${label || value}</label
50
- >
51
- `
52
- )
53
- )
54
- : ""}
55
- ${~valid && fctx.validate
56
- ? `<div class="input__error">${message}</div>`
57
- : ""}`
43
+ >
44
+ <ul>
45
+ ${isVisible
46
+ ? options.map(
47
+ ({ value, label }) => tempstream/* HTML */ `<li>
48
+ ${inputWrapper(
49
+ "checkbox",
50
+ [name],
51
+ /* HTML */ `
52
+ <input
53
+ type="checkbox"
54
+ id="${name}.${value}"
55
+ name="${name}.${value}"
56
+ ${pickedValues.includes(value)
57
+ ? "checked"
58
+ : ""}
59
+ form=${fctx.form_id}
60
+ />
61
+ <label for="${name}.${value}"
62
+ >${label || value}</label
63
+ >
64
+ `
65
+ )}
66
+ </li>`
67
+ )
68
+ : ""}
69
+ ${~valid && fctx.validate
70
+ ? `<div class="input__error">${message}</div>`
71
+ : ""}
72
+ </ul>`
58
73
  );
59
74
  }
60
75
  }
@@ -230,6 +230,7 @@ export class MultipleFiles extends FormFieldControl {
230
230
  ][];
231
231
 
232
232
  return inputWrapper(
233
+ "multiple-files",
233
234
  ["multiple-files", this.field.name, ...this.getClassModifiers()],
234
235
  tempstream/* HTML */ `
235
236
  <label>${this.options.label || this.field.name}</label>
@@ -137,6 +137,7 @@ export class SimpleInput<
137
137
  true
138
138
  );
139
139
  return inputWrapper(
140
+ "simple-input",
140
141
  this.getWrapperModifiers(),
141
142
  tempstream/* HTML */ `
142
143
  ${label ? `<label for="${id}">${label}</label>` : ""}
@@ -85,6 +85,7 @@ export abstract class Tickable<T> extends FormFieldControl {
85
85
  const required = this.field.required;
86
86
 
87
87
  return inputWrapper(
88
+ this.type,
88
89
  this.getWrapperClasses(fctx.ctx, fctx.data, parsed),
89
90
 
90
91
  /* HTML */ `
package/src/page/list.ts CHANGED
@@ -14,7 +14,7 @@ import { FormDataValue } from "../forms/form-types.js";
14
14
  export const BasePagePropsShape = <const>{};
15
15
  export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
16
16
 
17
- const DEFAULT_ITEMS_PER_PAGE = 12;
17
+ export const DEFAULT_ITEMS_PER_PAGE = 12;
18
18
 
19
19
  export const BaseListPageFields = <const>{
20
20
  page: new NumberField(false, 1),
@@ -0,0 +1,25 @@
1
+ import assert from "assert";
2
+ import { RealAppTest } from "../test_utils/test-on-real-app.js";
3
+
4
+ describe("sealious-list test", () => {
5
+ it("creates minimal sealious app and adds a simple route that works", async function () {
6
+ const test_app = await RealAppTest.init();
7
+
8
+ const url = "/list";
9
+ await test_app.runSealgenCommand(`add-collection --collection_name=samples`);
10
+ await test_app.runSealgenCommand(
11
+ `add-route --url=${url} --action=List --mode=list --collection=samples`
12
+ );
13
+ await test_app.start();
14
+ const res_post = await test_app.httpPOST("/api/v1/collections/samples", {
15
+ content: "some-content",
16
+ });
17
+ assert(res_post.status == 201);
18
+ const res = await test_app.httpGet(url);
19
+ if (res.status != 200) {
20
+ throw new Error("url generated by add-route exists but doesn't return 200");
21
+ }
22
+ assert(res.response.includes("some-content"));
23
+ await test_app.close();
24
+ }).timeout(100 * 1000);
25
+ });
@@ -1,5 +1,9 @@
1
1
  import { Context } from "koa";
2
- import { BaseListPageFields, ListPage } from "./list.js";
2
+ import {
3
+ BaseListPageFields,
4
+ DEFAULT_ITEMS_PER_PAGE,
5
+ ListPage,
6
+ } from "./list.js";
3
7
  import {
4
8
  Collection,
5
9
  CollectionItem,
@@ -7,7 +11,7 @@ import {
7
11
  Context as SealiousContext,
8
12
  CollectionInput,
9
13
  } from "sealious";
10
- import { FlatTemplatable } from "tempstream";
14
+ import { FlatTemplatable, Templatable, tempstream } from "tempstream";
11
15
  import { peopleWhoCan } from "./access-control.js";
12
16
  import qs from "qs";
13
17
  import { is, predicates } from "@sealcode/ts-predicates";
@@ -57,12 +61,12 @@ export abstract class SealiousItemListPage<
57
61
  async getItems(
58
62
  ctx: Context,
59
63
  page: number,
60
- itemsPerPage: number
64
+ itemsPerPage: number | null
61
65
  ): Promise<CollectionItem<C>[]> {
62
66
  const query = await this.makeQuery(ctx);
63
67
  await this.addFilter(ctx, query);
64
68
  const { items } = await query
65
- .paginate({ items: itemsPerPage, page })
69
+ .paginate({ items: itemsPerPage || DEFAULT_ITEMS_PER_PAGE, page })
66
70
  .fetch();
67
71
  return items;
68
72
  }
@@ -79,6 +83,10 @@ export abstract class SealiousItemListPage<
79
83
  return filter_values;
80
84
  }
81
85
 
86
+ renderListContainer(_: Context, content: Templatable): FlatTemplatable {
87
+ return tempstream`<div class="sealgen-sealious-list-container">${content}</div>`;
88
+ }
89
+
82
90
  async renderItem(
83
91
  _: Context,
84
92
  item: CollectionItem<C>,
@@ -6,6 +6,7 @@ import { extractCollectionClassname } from "../generate-collections.js";
6
6
  import extract_fields_from_collection from "../utils/extract-fields-from-collection.js";
7
7
  import { DefaultListFilters } from "../page/default-list-filters.js";
8
8
  import { formatWithPrettier } from "../utils/prettier.js";
9
+ import { toKebabCase } from "js-convert-case";
9
10
  const target_locreq = _locreq(process.cwd());
10
11
 
11
12
  export const listTemplate = async (
@@ -162,7 +163,7 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
162
163
  return html(
163
164
  ctx,
164
165
  "${action_name}",
165
- <div>
166
+ <div class="sealious-list-wrapper ${toKebabCase(action_name)}--wrapper">
166
167
  <h2>${action_name} List</h2>
167
168
  <table class="sealious-list">
168
169
  {this.renderTableHead(ctx, displayFields)}
@@ -0,0 +1,26 @@
1
+ /* eslint-disable @typescript-eslint/restrict-template-expressions */
2
+ import { exec } from "child_process";
3
+
4
+ export const execAsync = (cmd: string) => {
5
+ return new Promise<void>((resolve, reject) => {
6
+ const process = exec(cmd);
7
+
8
+ console.log("EXECUTING", cmd);
9
+
10
+ process?.stdout?.on("data", (data) => {
11
+ console.log(`stdout: ${data}`);
12
+ });
13
+
14
+ process?.stderr?.on("data", (data) => {
15
+ console.log(`stdout: ${data}`);
16
+ });
17
+
18
+ process.on("exit", (code) => {
19
+ if (code == 0) {
20
+ resolve();
21
+ } else {
22
+ reject();
23
+ }
24
+ });
25
+ });
26
+ };
@@ -0,0 +1,167 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
3
+ import { execAsync } from "./exec_async.js";
4
+ import { spawn } from "child_process";
5
+ import getPort from "get-port";
6
+ import waitPort from "wait-port";
7
+ import { promises as fs } from "fs";
8
+
9
+ import _locreq from "locreq";
10
+ import { module_dirname } from "../utils/module-dirname.js";
11
+ import EventEmitter from "events";
12
+ const locreq = _locreq(module_dirname(import.meta.url));
13
+
14
+ export const callSealgenBin = () => `node ${locreq.resolve("lib/cli.js")}`;
15
+
16
+ const sealious_minimal_commit = "b0d5bf5";
17
+
18
+ export class RealAppTest {
19
+ public node_pid: number;
20
+ public app_port: number;
21
+ public mongo_port: number;
22
+ public app_path: string;
23
+ public container_name: string;
24
+ public emitter = new EventEmitter();
25
+
26
+ static async init(): Promise<RealAppTest> {
27
+ const test_app = new RealAppTest();
28
+ test_app.node_pid = 0;
29
+ const timestamp = Date.now();
30
+ test_app.app_path = `/tmp/sealious-minimal-${timestamp}`;
31
+ test_app.container_name = `sealgen-test-mongo-${timestamp}`;
32
+ console.log("\tcloning sealious-minimal...");
33
+ await execAsync(
34
+ `git clone https://hub.sealcode.org/source/sealious-minimal.git ${test_app.app_path} && cd ${test_app.app_path} && git checkout ${sealious_minimal_commit}`
35
+ );
36
+ console.log("\tinstalling dependencies and sealgen...");
37
+ await execAsync(
38
+ `cd ${
39
+ test_app.app_path
40
+ } && npm ci rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen && ${callSealgenBin()} make-env`
41
+ );
42
+
43
+ console.log(
44
+ "\tstarting the database...(if timeouts here, run 'docker pull mongo:4.4-bionic' to solve)"
45
+ );
46
+ test_app.app_port = await getPort();
47
+ return test_app;
48
+ }
49
+
50
+ async start() {
51
+ this.mongo_port = await getPort();
52
+ await execAsync(
53
+ `docker run -p ${this.mongo_port}:27017 --name ${this.container_name} -d mongo:4.4-bionic`
54
+ );
55
+ const env = {
56
+ ...process.env,
57
+ SEALIOUS_MONGO_PORT: this.mongo_port.toString(),
58
+ SEALIOUS_PORT: this.app_port.toString(),
59
+ };
60
+ const node_args = [`${this.app_path}/dist/back/index.js`];
61
+ console.log(
62
+ "EXECUTING APP",
63
+ `${Object.entries(env)
64
+ .map(([key, value]) => `${key}=${value}`)
65
+ .join(" ")} node ${node_args.join(" ")}`
66
+ );
67
+ const app_start = spawn(`node`, node_args, {
68
+ env,
69
+ });
70
+ app_start.stdout.on("data", (data) =>
71
+ console.log("APP OUTPUT:", data.toString())
72
+ );
73
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
74
+ app_start.on("exit", (code) => {
75
+ this.emitter.emit("exit", code);
76
+ this.emitter.emit("process_error");
77
+ });
78
+ if (app_start.pid == undefined) throw new Error("node failed to start");
79
+ this.node_pid = app_start.pid;
80
+ await waitPort({
81
+ host: "localhost",
82
+ port: this.app_port,
83
+ output: "silent",
84
+ timeout: 6000,
85
+ });
86
+ }
87
+
88
+ async close() {
89
+ try {
90
+ if (this.node_pid != 0) await execAsync(`kill -9 ${this.node_pid}`);
91
+ } catch (error) {
92
+ throw new Error(
93
+ "node process exited unexpectedly, please inspect the output"
94
+ );
95
+ }
96
+ await Promise.all([
97
+ fs.rm(this.app_path, { recursive: true, force: true }),
98
+ execAsync(`docker rm -f ${this.container_name}`),
99
+ ]);
100
+ }
101
+
102
+ async runSealgenCommand(command: string) {
103
+ await execAsync(
104
+ `cd ${
105
+ this.app_path
106
+ } && ${callSealgenBin()} ${command} && ${callSealgenBin()} build`
107
+ );
108
+ }
109
+
110
+ async throwIfItKillsTheApp<T>(cb: () => Promise<T>): Promise<T> {
111
+ let finished = false;
112
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
113
+ return new Promise(async (resolve, reject) => {
114
+ const error_handler = () => {
115
+ if (!finished) {
116
+ reject();
117
+ }
118
+ finished = true;
119
+ };
120
+ this.emitter.addListener("process_error", error_handler);
121
+ try {
122
+ const result = await cb();
123
+ if (!finished) {
124
+ resolve(result);
125
+ finished = true;
126
+ }
127
+ } catch (e) {
128
+ if (!finished) {
129
+ reject(e);
130
+ finished = true;
131
+ }
132
+ }
133
+ this.emitter.removeListener("process_error", error_handler);
134
+ });
135
+ }
136
+
137
+ async httpGet(path: string): Promise<{ status: number; response: string }> {
138
+ return this.throwIfItKillsTheApp(async () => {
139
+ const ret = await fetch(`http://localhost:${this.app_port}${path}`);
140
+ const status = ret.status;
141
+ const response = await ret.text();
142
+ return { status, response };
143
+ });
144
+ }
145
+
146
+ async httpPOST(
147
+ path: string,
148
+ body: Record<string, unknown>
149
+ ): Promise<{ status: number; response: string }> {
150
+ return this.throwIfItKillsTheApp(async () => {
151
+ const ret = await fetch(
152
+ `http://localhost:${this.app_port}${path}`,
153
+ {
154
+ method: "POST",
155
+ headers: { "Content-Type": "application/json" },
156
+ body: JSON.stringify(body),
157
+ }
158
+ );
159
+ const status = ret.status;
160
+ const response = await ret.text();
161
+
162
+ console.log("request done!");
163
+ console.log({ response });
164
+ return { status, response };
165
+ });
166
+ }
167
+ }
@@ -10,11 +10,14 @@ function getModifierClasses(modifiers: string[]) {
10
10
  }
11
11
 
12
12
  export function inputWrapper(
13
+ type: string,
13
14
  modifiers: string[],
14
15
  input: FlatTemplatable
15
16
  ): Readable {
16
17
  return tempstream/* HTML */ `<div
17
- class="form-input form-input__wrapper ${getModifierClasses(modifiers)}"
18
+ class="form-input form-input--type-${type} form-input__wrapper ${getModifierClasses(
19
+ modifiers
20
+ )}"
18
21
  >
19
22
  ${input}
20
23
  </div>`;