@sealcode/sealgen 0.10.3 → 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.
- package/@types/generate-collections.d.ts +1 -0
- package/@types/index.d.ts +1 -0
- package/@types/page/default-list-filters.d.ts +6 -0
- package/@types/page/list.d.ts +1 -1
- package/@types/utils/extract-fields-from-collection.d.ts +4 -0
- package/lib/add-route.test.js +12 -4
- package/lib/build.js +0 -2
- package/lib/generate-collections.js +2 -1
- package/lib/index.js +1 -0
- package/lib/page/default-list-filters.js +45 -0
- package/lib/page/list.js +2 -2
- package/lib/templates/list.js +102 -22
- package/lib/utils/extract-fields-from-collection.js +32 -0
- package/package.json +1 -1
- package/src/add-route.test.ts +10 -3
- package/src/build.ts +0 -8
- package/src/generate-collections.ts +1 -1
- package/src/index.ts +1 -0
- package/src/page/default-list-filters.ts +46 -0
- package/src/page/list.ts +6 -6
- package/src/templates/list.ts +108 -23
- package/src/utils/extract-fields-from-collection.ts +32 -0
package/@types/index.d.ts
CHANGED
|
@@ -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>;
|
package/@types/page/list.d.ts
CHANGED
|
@@ -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;
|
package/lib/add-route.test.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
|
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) &&
|
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 =
|
|
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>
|
package/lib/templates/list.js
CHANGED
|
@@ -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
|
|
18
|
-
const uppercase_collection =
|
|
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
|
-
"${
|
|
84
|
+
"${action_name}",
|
|
44
85
|
tempstream/* HTML */ \`<div>
|
|
45
|
-
<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
|
|
64
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
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,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
package/src/add-route.test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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/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");
|
package/src/index.ts
CHANGED
|
@@ -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(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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";
|
package/src/templates/list.ts
CHANGED
|
@@ -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
|
|
21
|
-
const uppercase_collection =
|
|
22
|
-
|
|
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
|
-
"${
|
|
95
|
+
"${action_name}",
|
|
50
96
|
tempstream/* HTML */ \`<div>
|
|
51
|
-
<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
|
|
70
|
-
(
|
|
71
|
-
|
|
72
|
-
|
|
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,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
|
+
}
|