@sealcode/sealgen 0.10.2 → 0.10.4

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.
@@ -1 +1,2 @@
1
+ export declare function extractCollectionClassname(full_file_path: string): Promise<string>;
1
2
  export declare function generateCollections(): Promise<void>;
package/@types/index.d.ts CHANGED
@@ -22,3 +22,4 @@ export * as sanitize from "./sanitize";
22
22
  export * from "./forms/multiform";
23
23
  export * as Components from "./components/components";
24
24
  export * from "./mount";
25
+ export * from "./page/default-list-filters";
@@ -0,0 +1,6 @@
1
+ import { Field } from "sealious";
2
+ import { FlatTemplatable } from "tempstream";
3
+ export type ListFilterRender = (value: string, field: Field) => FlatTemplatable;
4
+ export declare const BooleanListFilter: (value: string, field: Field) => string;
5
+ export declare const SimpleTextFilter: (value: string, field: Field) => string;
6
+ export declare const DefaultListFilters: Record<string, ListFilterRender>;
@@ -35,7 +35,7 @@ export declare abstract class ListPage<ItemType, F extends typeof BaseListPageFi
35
35
  } | null>;
36
36
  makeSortLink(ctx: Context, field: string, order: "asc" | "desc"): string;
37
37
  renderFilters(ctx: Context): Promise<FlatTemplatable>;
38
- renderHeading(ctx: Context, field: string, label?: string): FlatTemplatable;
38
+ renderHeading(ctx: Context, field: string, label?: string): Promise<FlatTemplatable>;
39
39
  renderTableHead(ctx: Context, fields: {
40
40
  field: string;
41
41
  label?: string;
@@ -0,0 +1 @@
1
+ export declare function LongRunningProcessTemplate(action_name: string, newfilefullpath: string): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { formTemplate } from "./form";
2
+ import { LongRunningProcessTemplate } from "./long-running-process";
3
+ import { multiformTemplate } from "./multiform";
4
+ import { pageTemplate } from "./page";
5
+ import { postTemplate } from "./post";
6
+ import { redirectTemplate } from "./redirect";
7
+ import { statefulPageTemplate } from "./stateful-page";
8
+ export declare const Templates: {
9
+ page: typeof pageTemplate;
10
+ form: typeof formTemplate;
11
+ list: (action_name: string, newfilefullpath: string) => Promise<string>;
12
+ multiform: typeof multiformTemplate;
13
+ redirect: typeof redirectTemplate;
14
+ post: typeof postTemplate;
15
+ sreact: typeof statefulPageTemplate;
16
+ lpr: typeof LongRunningProcessTemplate;
17
+ };
@@ -0,0 +1,4 @@
1
+ export default function extract_fields_from_collection(collection_name: string): Promise<{
2
+ name: string;
3
+ type: string;
4
+ }[]>;
package/lib/add-route.js CHANGED
@@ -8,16 +8,10 @@ const fs_1 = require("fs");
8
8
  const locreq_1 = __importDefault(require("locreq"));
9
9
  const path_1 = require("path");
10
10
  const generate_routes_1 = require("./generate-routes");
11
- const form_1 = require("./templates/form");
12
- const list_1 = require("./templates/list");
13
- const multiform_1 = require("./templates/multiform");
14
- const page_1 = require("./templates/page");
15
- const redirect_1 = require("./templates/redirect");
16
11
  const route_test_1 = require("./templates/route-test");
17
12
  const escape_url_params_1 = require("./utils/escape-url-params");
18
13
  const prompts = require("prompts");
19
- const post_1 = require("./templates/post");
20
- const stateful_page_1 = require("./templates/stateful-page");
14
+ const templates_1 = require("./templates/templates");
21
15
  async function addRoute(params, app_directory = process.cwd()) {
22
16
  const target_locreq = (0, locreq_1.default)(app_directory);
23
17
  prompts.override(params);
@@ -46,21 +40,14 @@ async function addRoute(params, app_directory = process.cwd()) {
46
40
  { title: "multiform", value: "multiform" },
47
41
  { title: "redirect", value: "redirect" },
48
42
  { title: "raw POST", value: "post" },
43
+ { title: "Long Running Process status page", value: "lpr" },
49
44
  ],
50
45
  },
51
46
  ]);
52
47
  const { action, url, mode } = response;
53
48
  const file_path = target_locreq.resolve(`src/back/routes/${(0, escape_url_params_1.escape_url_params)(url)}.${mode}.ts`);
54
49
  const test_file_path = target_locreq.resolve(`src/back/routes/${(0, escape_url_params_1.escape_url_params)(url)}.test.ts`);
55
- const template = {
56
- page: page_1.pageTemplate,
57
- form: form_1.formTemplate,
58
- list: list_1.listTemplate,
59
- multiform: multiform_1.multiformTemplate,
60
- redirect: redirect_1.redirectTemplate,
61
- post: post_1.postTemplate,
62
- sreact: stateful_page_1.statefulPageTemplate,
63
- }[mode];
50
+ const template = templates_1.Templates[mode];
64
51
  if (!template) {
65
52
  throw new Error(`Could not get template ${mode}`);
66
53
  }
@@ -15,8 +15,13 @@ describe("add route test", () => {
15
15
  const app_path = "/tmp/sealious-minimal";
16
16
  const container_name = "sealgen-test-mongo";
17
17
  afterEach(async function () {
18
- if (node_pid != 0)
19
- await execAsync(`kill -9 ${node_pid}`);
18
+ try {
19
+ if (node_pid != 0)
20
+ await execAsync(`kill -9 ${node_pid}`);
21
+ }
22
+ catch (error) {
23
+ throw new Error("node process exited unexpectedly, please inspect the output");
24
+ }
20
25
  await Promise.all([
21
26
  fs_1.promises.rm(app_path, { recursive: true, force: true }),
22
27
  execAsync(`docker rm -f ${container_name}`),
@@ -25,7 +30,7 @@ describe("add route test", () => {
25
30
  it("creates minimal sealious app", async function () {
26
31
  this.timeout(250000);
27
32
  console.log("\tcloning sealious-minimal...");
28
- await execAsync(`git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout f48991a0a8b426e0d2f4a4b448fdd41d643b0dae`);
33
+ await execAsync(`git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout ce4f33309294`);
29
34
  console.log("\tinstalling dependencies and sealgen...");
30
35
  await execAsync(`cd ${app_path} && npm ci && rm -rf node_modules/@sealcode/sealgen && ln -s $OLDPWD node_modules/@sealcode/sealgen`);
31
36
  console.log("\trunning add-route and building app...");
@@ -38,6 +43,8 @@ describe("add route test", () => {
38
43
  let app_start = (0, child_process_1.spawn)(`node`, [`${app_path}/dist/back/index.js`], {
39
44
  env,
40
45
  });
46
+ app_start.stdout.on("data", (data) => console.log(data.toString()));
47
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
41
48
  if (app_start.pid == undefined)
42
49
  throw new Error("node failed to start");
43
50
  node_pid = app_start.pid;
@@ -48,8 +55,9 @@ describe("add route test", () => {
48
55
  timeout: 6000,
49
56
  });
50
57
  const res = await axios_1.default.get("http://localhost:8080/sample");
51
- if (res.status != 200)
58
+ if (res.status != 200) {
52
59
  throw new Error("url generated by add-route exists but doesnt return 200");
60
+ }
53
61
  });
54
62
  });
55
63
  //# sourceMappingURL=add-route.test.js.map
package/lib/build.js CHANGED
@@ -71,7 +71,6 @@ async function build_ts() {
71
71
  all_named
72
72
  .match(/[a-zA-Z_$][^,}]+/g)
73
73
  .map((importname) => {
74
- console.log(importname, `importname.startsWith("default as")`, importname.startsWith("default as"));
75
74
  if (importname.startsWith("default as")) {
76
75
  return `const ${importname.replace("default as ", "")} = ${new_name}?.default || ${new_name};`;
77
76
  }
@@ -83,7 +82,6 @@ async function build_ts() {
83
82
  return `const ${importname} = ${new_name}?.default?.${importname} || ${new_name}?.${importname};`;
84
83
  })
85
84
  .join(" ");
86
- console.log({ path, result });
87
85
  return result;
88
86
  });
89
87
  return { contents, loader: "ts" };
@@ -26,7 +26,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- exports.generateCollections = void 0;
29
+ exports.generateCollections = exports.extractCollectionClassname = void 0;
30
30
  const locreq_1 = __importDefault(require("locreq"));
31
31
  const prettier = __importStar(require("prettier"));
32
32
  const fs_1 = require("fs");
@@ -41,6 +41,7 @@ async function extractCollectionClassname(full_file_path) {
41
41
  }
42
42
  return (0, ts_predicates_1.assertType)(result[1], ts_predicates_1.predicates.string, `Missing export default class in '${full_file_path}'?`);
43
43
  }
44
+ exports.extractCollectionClassname = extractCollectionClassname;
44
45
  async function generateCollections() {
45
46
  const collections_dir = target_locreq.resolve("src/back/collections");
46
47
  const files = (await fs_1.promises.readdir(collections_dir)).filter((f) => !/\.(sub)?test\./.exec(f) &&
@@ -36,6 +36,7 @@ const import_path_1 = require("./utils/import-path");
36
36
  const ts_predicates_1 = require("@sealcode/ts-predicates");
37
37
  const escape_url_params_1 = require("./utils/escape-url-params");
38
38
  const prettier_1 = require("./utils/prettier");
39
+ const templates_1 = require("./templates/templates");
39
40
  const target_locreq = (0, locreq_1.default)(process.cwd());
40
41
  async function extractActionName(full_file_path) {
41
42
  const file_content = await fs_1.promises.readFile(full_file_path, "utf-8");
@@ -65,13 +66,7 @@ function generateURL(actionName, url) {
65
66
  }
66
67
  async function generateRoutes() {
67
68
  const files = await Promise.all((await (0, walk_1.walkDir)(target_locreq.resolve("src/back/routes")))
68
- .filter((f) => f.endsWith(".page.ts") ||
69
- f.endsWith(".form.ts") ||
70
- f.endsWith(".multiform.ts") ||
71
- f.endsWith(".list.ts") ||
72
- f.endsWith(".redirect.ts") ||
73
- f.endsWith(".post.ts") ||
74
- f.endsWith(".sreact.ts"))
69
+ .filter((f) => Object.keys(templates_1.Templates).some((key) => f.endsWith(`.${key}.ts`)))
75
70
  .map(async (fullpath) => ({
76
71
  fullpath,
77
72
  actionName: await extractActionName(fullpath),
package/lib/index.js CHANGED
@@ -42,4 +42,5 @@ exports.sanitize = __importStar(require("./sanitize"));
42
42
  __exportStar(require("./forms/multiform"), exports);
43
43
  exports.Components = __importStar(require("./components/components"));
44
44
  __exportStar(require("./mount"), exports);
45
+ __exportStar(require("./page/default-list-filters"), exports);
45
46
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DefaultListFilters = exports.SimpleTextFilter = exports.BooleanListFilter = void 0;
4
+ const BooleanListFilter = (value, field) => {
5
+ return /* HTML */ `<label>
6
+ ${field.name}
7
+ <select name="filter[${field.name}]" autocomplete="off">
8
+ <option value="true" ${value == "true" ? "selected" : ""}>
9
+ True
10
+ </option>
11
+ <option value="false" ${value == "false" ? "selected" : ""}>
12
+ False
13
+ </option>
14
+ <option
15
+ value=""
16
+ ${value == "null" ||
17
+ value == "" ||
18
+ value == "undefined" ||
19
+ value == undefined
20
+ ? "selected"
21
+ : ""}
22
+ >
23
+ -
24
+ </option>
25
+ </select>
26
+ </label>`;
27
+ };
28
+ exports.BooleanListFilter = BooleanListFilter;
29
+ const SimpleTextFilter = (value, field) => {
30
+ return /* HTML */ `<label>
31
+ ${field.name}
32
+ <input
33
+ type="text"
34
+ name="${field.name}"
35
+ ${value ? `value="${value}"` : value}
36
+ />
37
+ </label>`;
38
+ };
39
+ exports.SimpleTextFilter = SimpleTextFilter;
40
+ exports.DefaultListFilters = {
41
+ boolean: exports.BooleanListFilter,
42
+ text: exports.SimpleTextFilter,
43
+ fallback: exports.SimpleTextFilter,
44
+ };
45
+ //# sourceMappingURL=default-list-filters.js.map
package/lib/page/list.js CHANGED
@@ -157,8 +157,8 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
157
157
  })}
158
158
  </form>`;
159
159
  }
160
- renderHeading(ctx, field, label = field) {
161
- const current_sort = decodeSort(qs_1.default.parse(new URL(ctx.url, "https://www.example.com").search.slice(1))["sort"]);
160
+ async renderHeading(ctx, field, label = field) {
161
+ const current_sort = await this.getSort(ctx);
162
162
  const current_order = current_sort?.field == field ? current_sort.order : null;
163
163
  const order = current_order == "desc" ? "asc" : "desc";
164
164
  return /* HTML */ `<th>
@@ -1,9 +1,17 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.listTemplate = void 0;
7
+ const locreq_1 = __importDefault(require("locreq"));
4
8
  const import_path_1 = require("../utils/import-path");
5
9
  const prompts = require("prompts");
6
10
  const list_collections_1 = require("../utils/list-collections");
11
+ const generate_collections_1 = require("../generate-collections");
12
+ const extract_fields_from_collection_1 = __importDefault(require("../utils/extract-fields-from-collection"));
13
+ const default_list_filters_1 = require("../page/default-list-filters");
14
+ const target_locreq = (0, locreq_1.default)(process.cwd());
7
15
  const listTemplate = async (action_name, newfilefullpath) => {
8
16
  const response = await prompts({
9
17
  type: "autocomplete",
@@ -14,19 +22,55 @@ const listTemplate = async (action_name, newfilefullpath) => {
14
22
  value: collection,
15
23
  })),
16
24
  });
17
- const collection = response.collection;
18
- const uppercase_collection = collection[0].toLocaleUpperCase() + collection.slice(1);
25
+ const collection_name = response.collection;
26
+ const [uppercase_collection, collection_fields] = await Promise.all([
27
+ (0, generate_collections_1.extractCollectionClassname)(target_locreq.resolve("src/back/collections/" + collection_name + ".ts")),
28
+ (0, extract_fields_from_collection_1.default)(collection_name),
29
+ ]);
19
30
  const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
20
31
  return `import { Context } from "koa";
21
- import { CollectionItem } from "sealious";
22
- import { tempstream } from "tempstream";
32
+ import { CollectionItem, Field } from "sealious";
33
+ import { FlatTemplatable, tempstream } from "tempstream";
23
34
  import { ${uppercase_collection} } from "${rel("src/back/collections/collections.js")}";
24
35
  import html from "${rel("src/back/html.js")}";
25
- import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen";
36
+ import { SealiousItemListPage, BaseListPageFields, DefaultListFilters } from "@sealcode/sealgen";
37
+ import qs from "qs";
38
+ import { is, predicates } from "@sealcode/ts-predicates";
26
39
 
27
40
  export const actionName = "${action_name}";
28
41
 
29
- const filterFields = {};
42
+ const filterFields = [
43
+ ${collection_fields
44
+ .map((field) => ` { field: "${field.name}", render: ${default_list_filters_1.DefaultListFilters[field.type]
45
+ ? `DefaultListFilters["${field.type}"]`
46
+ : `DefaultListFilters.fallback`} }`)
47
+ .join(",\n")}
48
+ ] as {
49
+ field: keyof (typeof PatronEvents)["fields"];
50
+ render?: (value: string, field: Field) => string;
51
+ }[];
52
+
53
+ const displayFields = [
54
+ ${collection_fields
55
+ .map((field) => {
56
+ if (field.type == "boolean") {
57
+ return ` {
58
+ field: "${field.name}",
59
+ label: "${field.name}",
60
+ format: (v: boolean) => (v ? "YES" : "NO"),
61
+ }`;
62
+ }
63
+ if (field.type == "datetime") {
64
+ return ` {
65
+ field: "${field.name}",
66
+ label: "${field.name}",
67
+ format: (v: number) => {const d = new Date(); d.setTime(v); return d.toString();},
68
+ }`;
69
+ }
70
+ return ` { field: "${field.name}", label: "${field.name}" }`;
71
+ })
72
+ .join(",\n")}
73
+ ];
30
74
 
31
75
  export default new (class ${action_name}Page extends SealiousItemListPage<
32
76
  typeof ${uppercase_collection},
@@ -34,23 +78,14 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
34
78
  > {
35
79
  fields = BaseListPageFields;
36
80
 
37
- filterFields = filterFields;
38
- filterControls = [];
39
-
40
81
  async render(ctx: Context) {
41
82
  return html(
42
83
  ctx,
43
- "${uppercase_collection}",
84
+ "${action_name}",
44
85
  tempstream/* HTML */ \`<div>
45
- <h2>${uppercase_collection} List</h2>
86
+ <h2>${action_name}</h2>
46
87
  <table class="sealious-list">
47
- \${this.renderTableHead(ctx, [
48
- { field: "id" },
49
- ...Object.keys(Patrons.fields).map((field) => ({
50
- field,
51
- label: field,
52
- })),
53
- ])}
88
+ \${this.renderTableHead(ctx, displayFields)}
54
89
  <tbody>
55
90
  \${super.render(ctx)}
56
91
  </tbody>
@@ -59,11 +94,56 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
59
94
  );
60
95
  }
61
96
 
97
+ async renderFilters(ctx: Context): Promise<FlatTemplatable> {
98
+ const query_params = qs.parse(ctx.search.slice(1));
99
+ query_params.page = "1";
100
+ const filter_values = await this.getFilterValues(ctx);
101
+ return tempstream/* HTML */ \`<form>
102
+ \${Object.entries(query_params).map(([key, value]) => {
103
+ if (key == "filter") {
104
+ return "";
105
+ }
106
+ return /* HTML */ \`<input
107
+ type="hidden"
108
+ name="\${key}"
109
+ value="\${value}"
110
+ />\`;
111
+ })}
112
+ \${filterFields.map((field_name) => {
113
+ const filter =
114
+ FieldToFilter[this.collection.fields[field_name].typeName] ||
115
+ FieldToFilter.boolean;
116
+
117
+ return (
118
+ filter?.render(
119
+ filter_values[field_name] || "",
120
+ this.collection.fields[field_name]
121
+ ) || ""
122
+ );
123
+ })}
124
+ <input type="submit" />
125
+ </form>\`;
126
+ }
127
+
128
+ async getFilterValues(ctx: Context) {
129
+ const query_params = qs.parse(ctx.search.slice(1));
130
+ const _filter_values = query_params?.filter || {};
131
+ let filter_values: Record<string, string | undefined> = is(
132
+ _filter_values,
133
+ predicates.record(predicates.string, predicates.string)
134
+ )
135
+ ? _filter_values
136
+ : {};
137
+ return filter_values;
138
+ }
139
+
62
140
  async renderItem(_: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
63
- return tempstream\`<tr><td>\${item.id}</td>\${Object.keys(${uppercase_collection}.fields).map(
64
- (fieldname: keyof typeof ${uppercase_collection}["fields"]) =>
65
- tempstream\`<td>\${item.get(fieldname)}</td>\`
66
- )}</tr>\`;
141
+ return tempstream/* HTML */ \`<tr>
142
+ \${displayFields.map(({ field, format }) => {
143
+ const value = item.get(field as any);
144
+ return tempstream\`<td>\${format ? format(value) : value}</td>\`;
145
+ })}
146
+ </tr>\`;
67
147
  }
68
148
  })(${uppercase_collection});
69
149
  `;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LongRunningProcessTemplate = void 0;
4
+ const import_path_1 = require("../utils/import-path");
5
+ async function LongRunningProcessTemplate(action_name, newfilefullpath) {
6
+ const rel = (0, import_path_1.curryImportPath)(newfilefullpath);
7
+ return `import { Context } from "koa";
8
+ import { tempstream } from "tempstream";
9
+ import { Page } from "@sealcode/sealgen";
10
+ import html from "${rel("src/back/html.js")}";
11
+ import { LongRunningProcess } from "sealious";
12
+
13
+ export const actionName = "${action_name}";
14
+
15
+ export default new (class PatroniteAutoImportStatusPage extends Page {
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
+ async canAccess(_: Context) {
18
+ return { canAccess: true, message: "" };
19
+ }
20
+
21
+ public interval = 2000;
22
+
23
+ async render(ctx: Context) {
24
+ const lpr_id = ctx.params.id;
25
+ const lpr_data = LongRunningProcess.getByID(ctx.$context, lpr_id);
26
+ return html(
27
+ ctx,
28
+ "PatroniteAutoImportStatus",
29
+ tempstream/* HTML */ \`\${lpr_data.then(
30
+ (lpr_data) => tempstream/* HTML */ \`
31
+ <div>status: \${lpr_data.state}</div>
32
+ <div style="height: 20px; width: 100%">
33
+ <div
34
+ style="height: 100%; background-color: green; width: \${Math.round(
35
+ lpr_data.progress * 100
36
+ )}%"
37
+ ></div>
38
+ </div>
39
+ <div>
40
+ \${lpr_data.events.map(
41
+ (e) => /* HTML */ \`<div>\${e.message}</div>\`
42
+ )}
43
+ </div>
44
+ \${lpr_data.state !== "error" &&
45
+ lpr_data.state !== "finished"
46
+ ? /* HTML */ \`<script>
47
+ setTimeout(() => {
48
+ document.location = document.location;
49
+ }, \${this.interval});
50
+ </script>\`
51
+ : ""}
52
+ \`
53
+ )}\`
54
+ );
55
+ }
56
+ })();
57
+ `;
58
+ }
59
+ exports.LongRunningProcessTemplate = LongRunningProcessTemplate;
60
+ //# sourceMappingURL=long-running-process.js.map
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Templates = void 0;
4
+ const form_1 = require("./form");
5
+ const list_1 = require("./list");
6
+ const long_running_process_1 = require("./long-running-process");
7
+ const multiform_1 = require("./multiform");
8
+ const page_1 = require("./page");
9
+ const post_1 = require("./post");
10
+ const redirect_1 = require("./redirect");
11
+ const stateful_page_1 = require("./stateful-page");
12
+ exports.Templates = {
13
+ page: page_1.pageTemplate,
14
+ form: form_1.formTemplate,
15
+ list: list_1.listTemplate,
16
+ multiform: multiform_1.multiformTemplate,
17
+ redirect: redirect_1.redirectTemplate,
18
+ post: post_1.postTemplate,
19
+ sreact: stateful_page_1.statefulPageTemplate,
20
+ lpr: long_running_process_1.LongRunningProcessTemplate,
21
+ };
22
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const locreq_1 = __importDefault(require("locreq"));
7
+ const utils_1 = require("./utils");
8
+ const target_locreq = (0, locreq_1.default)(process.cwd());
9
+ const fs_1 = require("fs");
10
+ async function extract_fields_from_collection(collection_name) {
11
+ const rel = (0, utils_1.curryImportPath)(target_locreq.resolve("dist/back"));
12
+ const extractor_code = `import {default as Collection} from "${rel(`./back/collections/${collection_name}.js`)}";
13
+ const c = new Collection();
14
+ const fields = [];
15
+ for (const field_name in c.fields){
16
+ const field = c.fields[field_name];
17
+ let type = field.typeName;
18
+ if(["derived-value", "cached-value"].includes(type)){
19
+ type = field.virtual_field.typeName
20
+ }
21
+ fields.push({name: field_name, type})
22
+ }
23
+ console.log(JSON.stringify(fields));
24
+ `;
25
+ const extractor_code_path = target_locreq.resolve("dist/back/___extract_fields.js");
26
+ await fs_1.promises.writeFile(extractor_code_path, extractor_code);
27
+ const { stdout } = await (0, utils_1.exec)("node", [extractor_code_path]);
28
+ await fs_1.promises.unlink(extractor_code_path);
29
+ return JSON.parse(stdout);
30
+ }
31
+ exports.default = extract_fields_from_collection;
32
+ //# sourceMappingURL=extract-fields-from-collection.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealcode/sealgen",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Module to automate adding routes and collections to a sealious application",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -12,7 +12,11 @@ describe("add route test", () => {
12
12
  const app_path = "/tmp/sealious-minimal";
13
13
  const container_name = "sealgen-test-mongo";
14
14
  afterEach(async function () {
15
- if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
15
+ try {
16
+ if (node_pid != 0) await execAsync(`kill -9 ${node_pid}`);
17
+ } catch (error) {
18
+ throw new Error("node process exited unexpectedly, please inspect the output");
19
+ }
16
20
  await Promise.all([
17
21
  fs.rm(app_path, { recursive: true, force: true }),
18
22
  execAsync(`docker rm -f ${container_name}`),
@@ -23,7 +27,7 @@ describe("add route test", () => {
23
27
  this.timeout(250000);
24
28
  console.log("\tcloning sealious-minimal...");
25
29
  await execAsync(
26
- `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout f48991a0a8b426e0d2f4a4b448fdd41d643b0dae`
30
+ `git clone https://hub.sealcode.org/source/sealious-minimal.git ${app_path} && cd ${app_path} && git checkout ce4f33309294`
27
31
  );
28
32
  console.log("\tinstalling dependencies and sealgen...");
29
33
  await execAsync(
@@ -43,6 +47,8 @@ describe("add route test", () => {
43
47
  let app_start = spawn(`node`, [`${app_path}/dist/back/index.js`], {
44
48
  env,
45
49
  });
50
+ app_start.stdout.on("data", (data) => console.log(data.toString()));
51
+ app_start.stderr.on("data", (data) => console.log(data.toString()));
46
52
  if (app_start.pid == undefined) throw new Error("node failed to start");
47
53
  node_pid = app_start.pid;
48
54
  await waitPort({
@@ -52,7 +58,8 @@ describe("add route test", () => {
52
58
  timeout: 6000,
53
59
  });
54
60
  const res = await axios.get("http://localhost:8080/sample");
55
- if (res.status != 200)
61
+ if (res.status != 200) {
56
62
  throw new Error("url generated by add-route exists but doesnt return 200");
63
+ }
57
64
  });
58
65
  });
package/src/add-route.ts CHANGED
@@ -2,16 +2,10 @@ import { existsSync, promises as fs } from "fs";
2
2
  import _locreq from "locreq";
3
3
  import { resolve } from "path";
4
4
  import { generateRoutes } from "./generate-routes";
5
- import { formTemplate } from "./templates/form";
6
- import { listTemplate } from "./templates/list";
7
- import { multiformTemplate } from "./templates/multiform";
8
- import { pageTemplate } from "./templates/page";
9
- import { redirectTemplate } from "./templates/redirect";
10
5
  import { testTemplate } from "./templates/route-test";
11
6
  import { escape_url_params } from "./utils/escape-url-params";
12
7
  import prompts = require("prompts");
13
- import { postTemplate } from "./templates/post";
14
- import { statefulPageTemplate } from "./templates/stateful-page";
8
+ import { Templates } from "./templates/templates";
15
9
 
16
10
  export async function addRoute(
17
11
  params: Partial<{ [key in "action" | "url" | "mode"]: string }>,
@@ -47,6 +41,7 @@ export async function addRoute(
47
41
  { title: "multiform", value: "multiform" },
48
42
  { title: "redirect", value: "redirect" },
49
43
  { title: "raw POST", value: "post" },
44
+ { title: "Long Running Process status page", value: "lpr" },
50
45
  ],
51
46
  },
52
47
  ]);
@@ -59,15 +54,7 @@ export async function addRoute(
59
54
  const test_file_path = target_locreq.resolve(
60
55
  `src/back/routes/${escape_url_params(url)}.test.ts`
61
56
  );
62
- const template = {
63
- page: pageTemplate,
64
- form: formTemplate,
65
- list: listTemplate,
66
- multiform: multiformTemplate,
67
- redirect: redirectTemplate,
68
- post: postTemplate,
69
- sreact: statefulPageTemplate,
70
- }[mode];
57
+ const template = Templates[mode as keyof typeof Templates];
71
58
 
72
59
  if (!template) {
73
60
  throw new Error(`Could not get template ${mode}`);
package/src/build.ts CHANGED
@@ -66,13 +66,6 @@ async function build_ts() {
66
66
  all_named
67
67
  .match(/[a-zA-Z_$][^,}]+/g)
68
68
  .map((importname: string) => {
69
- console.log(
70
- importname,
71
- `importname.startsWith("default as")`,
72
- importname.startsWith(
73
- "default as"
74
- )
75
- );
76
69
  if (
77
70
  importname.startsWith(
78
71
  "default as"
@@ -98,7 +91,6 @@ async function build_ts() {
98
91
  return `const ${importname} = ${new_name}?.default?.${importname} || ${new_name}?.${importname};`;
99
92
  })
100
93
  .join(" ");
101
- console.log({ path, result });
102
94
  return result;
103
95
  }
104
96
  );
@@ -6,7 +6,7 @@ import { getPrettierConfig } from "./utils/prettier";
6
6
 
7
7
  const target_locreq = _locreq(process.cwd());
8
8
 
9
- async function extractCollectionClassname(
9
+ export async function extractCollectionClassname(
10
10
  full_file_path: string
11
11
  ): Promise<string> {
12
12
  const file_content = await fs.readFile(full_file_path, "utf-8");
@@ -7,6 +7,7 @@ import { importPath } from "./utils/import-path";
7
7
  import { assertType, predicates } from "@sealcode/ts-predicates";
8
8
  import { unescape_url_params } from "./utils/escape-url-params";
9
9
  import { getPrettierConfig } from "./utils/prettier";
10
+ import { Templates } from "./templates/templates";
10
11
 
11
12
  const target_locreq = _locreq(process.cwd());
12
13
 
@@ -49,15 +50,8 @@ export async function generateRoutes(): Promise<void> {
49
50
  (
50
51
  await walkDir(target_locreq.resolve("src/back/routes"))
51
52
  )
52
- .filter(
53
- (f) =>
54
- f.endsWith(".page.ts") ||
55
- f.endsWith(".form.ts") ||
56
- f.endsWith(".multiform.ts") ||
57
- f.endsWith(".list.ts") ||
58
- f.endsWith(".redirect.ts") ||
59
- f.endsWith(".post.ts") ||
60
- f.endsWith(".sreact.ts")
53
+ .filter((f) =>
54
+ Object.keys(Templates).some((key) => f.endsWith(`.${key}.ts`))
61
55
  )
62
56
  .map(async (fullpath) => ({
63
57
  fullpath,
package/src/index.ts CHANGED
@@ -24,3 +24,4 @@ export * as sanitize from "./sanitize";
24
24
  export * from "./forms/multiform";
25
25
  export * as Components from "./components/components";
26
26
  export * from "./mount";
27
+ export * from "./page/default-list-filters";
@@ -0,0 +1,46 @@
1
+ import { Field } from "sealious";
2
+ import { FlatTemplatable } from "tempstream";
3
+
4
+ export type ListFilterRender = (value: string, field: Field) => FlatTemplatable;
5
+
6
+ export const BooleanListFilter = (value: string, field: Field) => {
7
+ return /* HTML */ `<label>
8
+ ${field.name}
9
+ <select name="filter[${field.name}]" autocomplete="off">
10
+ <option value="true" ${value == "true" ? "selected" : ""}>
11
+ True
12
+ </option>
13
+ <option value="false" ${value == "false" ? "selected" : ""}>
14
+ False
15
+ </option>
16
+ <option
17
+ value=""
18
+ ${value == "null" ||
19
+ value == "" ||
20
+ value == "undefined" ||
21
+ value == undefined
22
+ ? "selected"
23
+ : ""}
24
+ >
25
+ -
26
+ </option>
27
+ </select>
28
+ </label>`;
29
+ };
30
+
31
+ export const SimpleTextFilter = (value: string, field: Field) => {
32
+ return /* HTML */ `<label>
33
+ ${field.name}
34
+ <input
35
+ type="text"
36
+ name="${field.name}"
37
+ ${value ? `value="${value}"` : value}
38
+ />
39
+ </label>`;
40
+ };
41
+
42
+ export const DefaultListFilters = {
43
+ boolean: BooleanListFilter,
44
+ text: SimpleTextFilter,
45
+ fallback: SimpleTextFilter,
46
+ } as Record<string, ListFilterRender>;
package/src/page/list.ts CHANGED
@@ -220,12 +220,12 @@ ${this.renderListContainer(ctx, this.renderItems(ctx, values))}`;
220
220
  </form>`;
221
221
  }
222
222
 
223
- renderHeading(ctx: Context, field: string, label = field): FlatTemplatable {
224
- const current_sort = decodeSort(
225
- qs.parse(
226
- new URL(ctx.url, "https://www.example.com").search.slice(1)
227
- )["sort"]
228
- );
223
+ async renderHeading(
224
+ ctx: Context,
225
+ field: string,
226
+ label = field
227
+ ): Promise<FlatTemplatable> {
228
+ const current_sort = await this.getSort(ctx);
229
229
  const current_order =
230
230
  current_sort?.field == field ? current_sort.order : null;
231
231
  const order = current_order == "desc" ? "asc" : "desc";
@@ -1,6 +1,11 @@
1
+ import _locreq from "locreq";
1
2
  import { curryImportPath } from "../utils/import-path";
2
3
  import prompts = require("prompts");
3
4
  import { listCollections } from "../utils/list-collections";
5
+ import { extractCollectionClassname } from "../generate-collections";
6
+ import extract_fields_from_collection from "../utils/extract-fields-from-collection";
7
+ import { DefaultListFilters } from "../page/default-list-filters";
8
+ const target_locreq = _locreq(process.cwd());
4
9
 
5
10
  export const listTemplate = async (
6
11
  action_name: string,
@@ -17,22 +22,66 @@ export const listTemplate = async (
17
22
  value: collection,
18
23
  })),
19
24
  });
20
- const collection = response.collection as string;
21
- const uppercase_collection =
22
- collection[0].toLocaleUpperCase() + collection.slice(1);
25
+ const collection_name = response.collection as string;
26
+ const [uppercase_collection, collection_fields] = await Promise.all([
27
+ extractCollectionClassname(
28
+ target_locreq.resolve(
29
+ "src/back/collections/" + collection_name + ".ts"
30
+ )
31
+ ),
32
+ extract_fields_from_collection(collection_name),
33
+ ]);
23
34
  const rel = curryImportPath(newfilefullpath);
24
35
  return `import { Context } from "koa";
25
- import { CollectionItem } from "sealious";
26
- import { tempstream } from "tempstream";
36
+ import { CollectionItem, Field } from "sealious";
37
+ import { FlatTemplatable, tempstream } from "tempstream";
27
38
  import { ${uppercase_collection} } from "${rel(
28
39
  "src/back/collections/collections.js"
29
40
  )}";
30
41
  import html from "${rel("src/back/html.js")}";
31
- import { SealiousItemListPage, BaseListPageFields } from "@sealcode/sealgen";
42
+ import { SealiousItemListPage, BaseListPageFields, DefaultListFilters } from "@sealcode/sealgen";
43
+ import qs from "qs";
44
+ import { is, predicates } from "@sealcode/ts-predicates";
32
45
 
33
46
  export const actionName = "${action_name}";
34
47
 
35
- const filterFields = {};
48
+ const filterFields = [
49
+ ${collection_fields
50
+ .map(
51
+ (field) =>
52
+ ` { field: "${field.name}", render: ${
53
+ DefaultListFilters[field.type]
54
+ ? `DefaultListFilters["${field.type}"]`
55
+ : `DefaultListFilters.fallback`
56
+ } }`
57
+ )
58
+ .join(",\n")}
59
+ ] as {
60
+ field: keyof (typeof PatronEvents)["fields"];
61
+ render?: (value: string, field: Field) => string;
62
+ }[];
63
+
64
+ const displayFields = [
65
+ ${collection_fields
66
+ .map((field) => {
67
+ if (field.type == "boolean") {
68
+ return ` {
69
+ field: "${field.name}",
70
+ label: "${field.name}",
71
+ format: (v: boolean) => (v ? "YES" : "NO"),
72
+ }`;
73
+ }
74
+ if (field.type == "datetime") {
75
+ return ` {
76
+ field: "${field.name}",
77
+ label: "${field.name}",
78
+ format: (v: number) => {const d = new Date(); d.setTime(v); return d.toString();},
79
+ }`;
80
+ }
81
+ return ` { field: "${field.name}", label: "${field.name}" }`;
82
+ })
83
+ .join(",\n")}
84
+ ];
36
85
 
37
86
  export default new (class ${action_name}Page extends SealiousItemListPage<
38
87
  typeof ${uppercase_collection},
@@ -40,23 +89,14 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
40
89
  > {
41
90
  fields = BaseListPageFields;
42
91
 
43
- filterFields = filterFields;
44
- filterControls = [];
45
-
46
92
  async render(ctx: Context) {
47
93
  return html(
48
94
  ctx,
49
- "${uppercase_collection}",
95
+ "${action_name}",
50
96
  tempstream/* HTML */ \`<div>
51
- <h2>${uppercase_collection} List</h2>
97
+ <h2>${action_name}</h2>
52
98
  <table class="sealious-list">
53
- \${this.renderTableHead(ctx, [
54
- { field: "id" },
55
- ...Object.keys(Patrons.fields).map((field) => ({
56
- field,
57
- label: field,
58
- })),
59
- ])}
99
+ \${this.renderTableHead(ctx, displayFields)}
60
100
  <tbody>
61
101
  \${super.render(ctx)}
62
102
  </tbody>
@@ -65,11 +105,56 @@ export default new (class ${action_name}Page extends SealiousItemListPage<
65
105
  );
66
106
  }
67
107
 
108
+ async renderFilters(ctx: Context): Promise<FlatTemplatable> {
109
+ const query_params = qs.parse(ctx.search.slice(1));
110
+ query_params.page = "1";
111
+ const filter_values = await this.getFilterValues(ctx);
112
+ return tempstream/* HTML */ \`<form>
113
+ \${Object.entries(query_params).map(([key, value]) => {
114
+ if (key == "filter") {
115
+ return "";
116
+ }
117
+ return /* HTML */ \`<input
118
+ type="hidden"
119
+ name="\${key}"
120
+ value="\${value}"
121
+ />\`;
122
+ })}
123
+ \${filterFields.map((field_name) => {
124
+ const filter =
125
+ FieldToFilter[this.collection.fields[field_name].typeName] ||
126
+ FieldToFilter.boolean;
127
+
128
+ return (
129
+ filter?.render(
130
+ filter_values[field_name] || "",
131
+ this.collection.fields[field_name]
132
+ ) || ""
133
+ );
134
+ })}
135
+ <input type="submit" />
136
+ </form>\`;
137
+ }
138
+
139
+ async getFilterValues(ctx: Context) {
140
+ const query_params = qs.parse(ctx.search.slice(1));
141
+ const _filter_values = query_params?.filter || {};
142
+ let filter_values: Record<string, string | undefined> = is(
143
+ _filter_values,
144
+ predicates.record(predicates.string, predicates.string)
145
+ )
146
+ ? _filter_values
147
+ : {};
148
+ return filter_values;
149
+ }
150
+
68
151
  async renderItem(_: Context, item: CollectionItem<typeof ${uppercase_collection}>) {
69
- return tempstream\`<tr><td>\${item.id}</td>\${Object.keys(${uppercase_collection}.fields).map(
70
- (fieldname: keyof typeof ${uppercase_collection}["fields"]) =>
71
- tempstream\`<td>\${item.get(fieldname)}</td>\`
72
- )}</tr>\`;
152
+ return tempstream/* HTML */ \`<tr>
153
+ \${displayFields.map(({ field, format }) => {
154
+ const value = item.get(field as any);
155
+ return tempstream\`<td>\${format ? format(value) : value}</td>\`;
156
+ })}
157
+ </tr>\`;
73
158
  }
74
159
  })(${uppercase_collection});
75
160
  `;
@@ -0,0 +1,59 @@
1
+ import { curryImportPath } from "../utils/import-path";
2
+
3
+ export async function LongRunningProcessTemplate(
4
+ action_name: string,
5
+ newfilefullpath: string
6
+ ): Promise<string> {
7
+ const rel = curryImportPath(newfilefullpath);
8
+ return `import { Context } from "koa";
9
+ import { tempstream } from "tempstream";
10
+ import { Page } from "@sealcode/sealgen";
11
+ import html from "${rel("src/back/html.js")}";
12
+ import { LongRunningProcess } from "sealious";
13
+
14
+ export const actionName = "${action_name}";
15
+
16
+ export default new (class PatroniteAutoImportStatusPage extends Page {
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ async canAccess(_: Context) {
19
+ return { canAccess: true, message: "" };
20
+ }
21
+
22
+ public interval = 2000;
23
+
24
+ async render(ctx: Context) {
25
+ const lpr_id = ctx.params.id;
26
+ const lpr_data = LongRunningProcess.getByID(ctx.$context, lpr_id);
27
+ return html(
28
+ ctx,
29
+ "PatroniteAutoImportStatus",
30
+ tempstream/* HTML */ \`\${lpr_data.then(
31
+ (lpr_data) => tempstream/* HTML */ \`
32
+ <div>status: \${lpr_data.state}</div>
33
+ <div style="height: 20px; width: 100%">
34
+ <div
35
+ style="height: 100%; background-color: green; width: \${Math.round(
36
+ lpr_data.progress * 100
37
+ )}%"
38
+ ></div>
39
+ </div>
40
+ <div>
41
+ \${lpr_data.events.map(
42
+ (e) => /* HTML */ \`<div>\${e.message}</div>\`
43
+ )}
44
+ </div>
45
+ \${lpr_data.state !== "error" &&
46
+ lpr_data.state !== "finished"
47
+ ? /* HTML */ \`<script>
48
+ setTimeout(() => {
49
+ document.location = document.location;
50
+ }, \${this.interval});
51
+ </script>\`
52
+ : ""}
53
+ \`
54
+ )}\`
55
+ );
56
+ }
57
+ })();
58
+ `;
59
+ }
@@ -0,0 +1,19 @@
1
+ import { formTemplate } from "./form";
2
+ import { listTemplate } from "./list";
3
+ import { LongRunningProcessTemplate } from "./long-running-process";
4
+ import { multiformTemplate } from "./multiform";
5
+ import { pageTemplate } from "./page";
6
+ import { postTemplate } from "./post";
7
+ import { redirectTemplate } from "./redirect";
8
+ import { statefulPageTemplate } from "./stateful-page";
9
+
10
+ export const Templates = {
11
+ page: pageTemplate,
12
+ form: formTemplate,
13
+ list: listTemplate,
14
+ multiform: multiformTemplate,
15
+ redirect: redirectTemplate,
16
+ post: postTemplate,
17
+ sreact: statefulPageTemplate,
18
+ lpr: LongRunningProcessTemplate,
19
+ };
@@ -0,0 +1,32 @@
1
+ import _locreq from "locreq";
2
+ import { curryImportPath, exec } from "./utils";
3
+ const target_locreq = _locreq(process.cwd());
4
+ import { promises as fs } from "fs";
5
+
6
+ export default async function extract_fields_from_collection(
7
+ collection_name: string
8
+ ): Promise<{ name: string; type: string }[]> {
9
+ const rel = curryImportPath(target_locreq.resolve("dist/back"));
10
+ const extractor_code = `import {default as Collection} from "${rel(
11
+ `./back/collections/${collection_name}.js`
12
+ )}";
13
+ const c = new Collection();
14
+ const fields = [];
15
+ for (const field_name in c.fields){
16
+ const field = c.fields[field_name];
17
+ let type = field.typeName;
18
+ if(["derived-value", "cached-value"].includes(type)){
19
+ type = field.virtual_field.typeName
20
+ }
21
+ fields.push({name: field_name, type})
22
+ }
23
+ console.log(JSON.stringify(fields));
24
+ `;
25
+ const extractor_code_path = target_locreq.resolve(
26
+ "dist/back/___extract_fields.js"
27
+ );
28
+ await fs.writeFile(extractor_code_path, extractor_code);
29
+ const { stdout } = await exec("node", [extractor_code_path]);
30
+ await fs.unlink(extractor_code_path);
31
+ return JSON.parse(stdout);
32
+ }