@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.
- package/@types/src/add-collection.d.ts +1 -1
- package/@types/src/page/list.d.ts +1 -0
- package/@types/src/page/sealious-list.d.ts +3 -2
- package/@types/src/page/sealious-list.test.d.ts +1 -0
- package/@types/src/test_utils/exec_async.d.ts +1 -0
- package/@types/src/test_utils/test-on-real-app.d.ts +24 -0
- package/@types/src/utils/input-wrapper.d.ts +1 -1
- package/lib/add-collection.js +18 -8
- package/lib/add-route.test.js +9 -92
- package/lib/forms/controls/checkboxed-list-input.js +31 -16
- package/lib/forms/controls/multiple-files.js +1 -0
- package/lib/forms/controls/simple-input.js +1 -0
- package/lib/forms/controls/tickable.js +1 -0
- package/lib/page/list.js +1 -0
- package/lib/page/sealious-list.js +9 -2
- package/lib/page/sealious-list.test.js +24 -0
- package/lib/templates/list.js +2 -1
- package/lib/test_utils/exec_async.js +24 -0
- package/lib/test_utils/test-on-real-app.js +147 -0
- package/lib/utils/input-wrapper.js +4 -2
- package/package.json +1 -1
- package/src/add-collection.ts +23 -8
- package/src/add-route.test.ts +9 -99
- package/src/forms/controls/checkboxed-list-input.ts +40 -25
- package/src/forms/controls/multiple-files.ts +1 -0
- package/src/forms/controls/simple-input.ts +1 -0
- package/src/forms/controls/tickable.ts +1 -0
- package/src/page/list.ts +1 -1
- package/src/page/sealious-list.test.ts +25 -0
- package/src/page/sealious-list.ts +12 -4
- package/src/templates/list.ts +2 -1
- package/src/test_utils/exec_async.ts +26 -0
- package/src/test_utils/test-on-real-app.ts +167 -0
- 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;
|
package/lib/add-collection.js
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
);
|
package/lib/add-route.test.js
CHANGED
|
@@ -1,98 +1,15 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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>
|
package/lib/page/list.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
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
|
package/lib/templates/list.js
CHANGED
|
@@ -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(
|
|
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
package/src/add-collection.ts
CHANGED
|
@@ -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(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
);
|
package/src/add-route.test.ts
CHANGED
|
@@ -1,106 +1,16 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
await
|
|
57
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/* HTML */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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>
|
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 {
|
|
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>,
|
package/src/templates/list.ts
CHANGED
|
@@ -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(
|
|
18
|
+
class="form-input form-input--type-${type} form-input__wrapper ${getModifierClasses(
|
|
19
|
+
modifiers
|
|
20
|
+
)}"
|
|
18
21
|
>
|
|
19
22
|
${input}
|
|
20
23
|
</div>`;
|