@sealcode/sealgen 0.8.48 → 0.8.49
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/.log/ti-107027.log +576 -0
- package/.log/ti-129414.log +1355 -0
- package/.log/ti-135707.log +1049 -0
- package/.log/ti-136226.log +1049 -0
- package/.log/ti-147652.log +533 -0
- package/.log/ti-157088.log +2938 -0
- package/.log/ti-250667.log +548 -0
- package/.log/ti-29438.log +165 -0
- package/.log/ti-29894.log +149 -0
- package/.log/ti-33785.log +149 -0
- package/.log/ti-34262.log +149 -0
- package/.log/ti-35719.log +149 -0
- package/.log/ti-41629.log +518 -0
- package/.log/ti-441248.log +517 -0
- package/.log/ti-5352.log +517 -0
- package/.log/ti-76335.log +639 -0
- package/.log/ti-811639.log +517 -0
- package/.log/ti-81805.log +564 -0
- package/.log/ti-86459.log +517 -0
- package/.log/tsserver.log +6881 -0
- package/.log/tsserver.sync-conflict-20221010-222251-JUX7UR2.log +9412 -0
- package/package.json +1 -1
- package/src/add-collection.ts +31 -0
- package/src/add-route.test.ts +58 -0
- package/src/add-route.ts +89 -0
- package/src/build.ts +94 -0
- package/src/cli.ts +36 -0
- package/src/components/components.ts +2 -0
- package/src/components/tabs.ts +95 -0
- package/src/components/toggle-modal.ts +52 -0
- package/src/example.test.ts +3 -0
- package/src/find-scss-includes.ts +32 -0
- package/src/forms/controls/array.ts +226 -0
- package/src/forms/controls/autocomplete.ts +33 -0
- package/src/forms/controls/big-radio-group.ts +48 -0
- package/src/forms/controls/checkbox-group.ts +71 -0
- package/src/forms/controls/checkbox.ts +34 -0
- package/src/forms/controls/checkboxed-list-input.ts +50 -0
- package/src/forms/controls/collapsible-checkbox-group.ts +28 -0
- package/src/forms/controls/collapsible-field-group.ts +94 -0
- package/src/forms/controls/controls.ts +30 -0
- package/src/forms/controls/derived-frame.ts +91 -0
- package/src/forms/controls/derived.ts +108 -0
- package/src/forms/controls/dropdown.ts +87 -0
- package/src/forms/controls/editable-collection-subset.ts +97 -0
- package/src/forms/controls/field-group.ts +69 -0
- package/src/forms/controls/file.ts +47 -0
- package/src/forms/controls/form-control.ts +24 -0
- package/src/forms/controls/form-field-control.ts +22 -0
- package/src/forms/controls/form-header.ts +19 -0
- package/src/forms/controls/form-paragraph.ts +11 -0
- package/src/forms/controls/frame.ts +26 -0
- package/src/forms/controls/hidden-input.ts +22 -0
- package/src/forms/controls/html.ts +38 -0
- package/src/forms/controls/multiple-files.ts +256 -0
- package/src/forms/controls/multiple-photos.ts +44 -0
- package/src/forms/controls/number.ts +38 -0
- package/src/forms/controls/photo.ts +74 -0
- package/src/forms/controls/radio.ts +43 -0
- package/src/forms/controls/reverse-single-reference.ts +50 -0
- package/src/forms/controls/simple-input.ts +148 -0
- package/src/forms/controls/textarea.ts +60 -0
- package/src/forms/controls/tickable.ts +127 -0
- package/src/forms/fields/boolean.test.ts +36 -0
- package/src/forms/fields/boolean.ts +63 -0
- package/src/forms/fields/checkboxed-list.ts +66 -0
- package/src/forms/fields/collection-field.ts +62 -0
- package/src/forms/fields/email.test.ts +24 -0
- package/src/forms/fields/email.ts +20 -0
- package/src/forms/fields/enum-multiple.test.ts +31 -0
- package/src/forms/fields/enum-multiple.ts +62 -0
- package/src/forms/fields/enum.test.ts +25 -0
- package/src/forms/fields/enum.ts +56 -0
- package/src/forms/fields/existing-sealious-value.ts +22 -0
- package/src/forms/fields/field.ts +172 -0
- package/src/forms/fields/file.ts +89 -0
- package/src/forms/fields/multiple-files.ts +20 -0
- package/src/forms/fields/number.test.ts +34 -0
- package/src/forms/fields/number.ts +70 -0
- package/src/forms/fields/pick-from-list.test.ts +19 -0
- package/src/forms/fields/pick-from-list.ts +52 -0
- package/src/forms/fields/proxy-field.ts +24 -0
- package/src/forms/fields/regexp.test.ts +25 -0
- package/src/forms/fields/regexp.ts +53 -0
- package/src/forms/fields/reverse-single-reference.ts +20 -0
- package/src/forms/fields/simple-form-field.ts +30 -0
- package/src/forms/fields/structured-array.ts +27 -0
- package/src/forms/form.test.ts +351 -0
- package/src/forms/form.ts +294 -0
- package/src/forms/forms.scss +85 -0
- package/src/forms/multiform.test.ts +540 -0
- package/src/forms/multiform.ts +236 -0
- package/src/generate-collections.ts +77 -0
- package/src/generate-routes.ts +121 -0
- package/src/index.ts +25 -0
- package/src/make-env.ts +15 -0
- package/src/make-hidden-inputs.ts +24 -0
- package/src/mount.ts +85 -0
- package/src/page/access-control.ts +18 -0
- package/src/page/list.ts +240 -0
- package/src/page/mountable-with-fields.ts +158 -0
- package/src/page/mountable.ts +13 -0
- package/src/page/page.ts +46 -0
- package/src/page/props-parser.ts +38 -0
- package/src/page/stateful-page.ts +202 -0
- package/src/run.ts +0 -0
- package/src/sanitize.ts +9 -0
- package/src/templates/collection.ts +11 -0
- package/src/templates/form.ts +60 -0
- package/src/templates/list.ts +75 -0
- package/src/templates/multiform.ts +58 -0
- package/src/templates/page.ts +25 -0
- package/src/templates/post.ts +21 -0
- package/src/templates/redirect.ts +22 -0
- package/src/templates/route-test.ts +27 -0
- package/src/templates/stateful-page.ts +65 -0
- package/src/test_utils/locator_is_visible.ts +17 -0
- package/src/utils/assert-throws.ts +18 -0
- package/src/utils/base64.ts +7 -0
- package/src/utils/browser-creator.js +10 -0
- package/src/utils/escape-url-params.ts +14 -0
- package/src/utils/exec.ts +30 -0
- package/src/utils/format-external-url.ts +13 -0
- package/src/utils/import-path.ts +25 -0
- package/src/utils/input-wrapper.ts +21 -0
- package/src/utils/list-collections.ts +16 -0
- package/src/utils/make-slug.ts +5 -0
- package/src/utils/natural-numbers.ts +5 -0
- package/src/utils/prettier.ts +13 -0
- package/src/utils/question.ts +23 -0
- package/src/utils/render-attributes.ts +17 -0
- package/src/utils/sleep.ts +3 -0
- package/src/utils/translate.ts +13 -0
- package/src/utils/url-with-new-params.ts +9 -0
- package/src/utils/utils.ts +23 -0
- package/src/utils/walk.ts +20 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
3
|
+
import { Context } from "koa";
|
|
4
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
5
|
+
import { Form, FormDataValue, FormMessage } from "..";
|
|
6
|
+
import { Fields, MountableWithFields } from "../page/mountable-with-fields";
|
|
7
|
+
import { attribute } from "../sanitize";
|
|
8
|
+
|
|
9
|
+
const FIELD_PREFIX_SEPARATOR = "___";
|
|
10
|
+
|
|
11
|
+
export class Multiform extends MountableWithFields {
|
|
12
|
+
controls = [];
|
|
13
|
+
public name: string;
|
|
14
|
+
public forms: Record<string, Form<Fields, unknown>>;
|
|
15
|
+
|
|
16
|
+
init(): void {
|
|
17
|
+
super.init();
|
|
18
|
+
for (const [key, form] of Object.entries(this.forms)) {
|
|
19
|
+
if (key !== attribute(key)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Form name "${key}" is not url-safe. Try: "${attribute(
|
|
22
|
+
key
|
|
23
|
+
)}"`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
form.makeOpenFormTag = () => `<div>`;
|
|
27
|
+
form.makeCloseFormTag = () => `</div>`;
|
|
28
|
+
form.field_names_prefix = key + FIELD_PREFIX_SEPARATOR;
|
|
29
|
+
form.form_id = this.name;
|
|
30
|
+
form.action = "./" + key;
|
|
31
|
+
form.init();
|
|
32
|
+
form.extractRawValues = async (context) =>
|
|
33
|
+
this.getSubformRawValues(context, key);
|
|
34
|
+
const submit_button_id = this.name + "_" + key + "_submit";
|
|
35
|
+
|
|
36
|
+
form.makeSubmitButton = () => /* HTML */ `<input
|
|
37
|
+
type="submit"
|
|
38
|
+
value="${form.submitButtonText}"
|
|
39
|
+
formaction="${form.action}"
|
|
40
|
+
id="${submit_button_id}"
|
|
41
|
+
${form.form_id ? `form="${form.form_id}"` : ""}
|
|
42
|
+
/> `;
|
|
43
|
+
}
|
|
44
|
+
for (const form of Object.values(this.forms)) {
|
|
45
|
+
for (const field of Object.values(form.fields)) {
|
|
46
|
+
if (field.name.includes(FIELD_PREFIX_SEPARATOR)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`A field name within multiform cannot contain '${FIELD_PREFIX_SEPARATOR}'`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async extractRawValues(
|
|
56
|
+
ctx: Context
|
|
57
|
+
): Promise<Record<string, FormDataValue>> {
|
|
58
|
+
return ctx.$body || {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async canAccess(): Promise<{ canAccess: boolean; message: string }> {
|
|
62
|
+
return {
|
|
63
|
+
canAccess: true,
|
|
64
|
+
message:
|
|
65
|
+
"this view includes multiple forms and each of them have their unique access rules",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
getSubformsToRender(
|
|
71
|
+
ctx: Context
|
|
72
|
+
): Array<readonly [string, Form<any, unknown>]> {
|
|
73
|
+
const requested_frame = ctx.headers["turbo-frame"];
|
|
74
|
+
if (
|
|
75
|
+
!is(
|
|
76
|
+
requested_frame,
|
|
77
|
+
predicates.or(predicates.undefined, predicates.string)
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
throw new Error("Wrong turbo-frame header value type");
|
|
81
|
+
}
|
|
82
|
+
const forms_to_render =
|
|
83
|
+
requested_frame && this.forms[requested_frame]
|
|
84
|
+
? [<const>[requested_frame, this.forms[requested_frame]]]
|
|
85
|
+
: Object.entries(this.forms);
|
|
86
|
+
return forms_to_render;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async renderSubform(
|
|
90
|
+
ctx: Context,
|
|
91
|
+
sub_form_name: string,
|
|
92
|
+
show_field_errors: boolean
|
|
93
|
+
): Promise<FlatTemplatable> {
|
|
94
|
+
const form = this.forms[sub_form_name];
|
|
95
|
+
const result = await form.canAccess(ctx);
|
|
96
|
+
if (!result.canAccess) {
|
|
97
|
+
return result.message;
|
|
98
|
+
}
|
|
99
|
+
return form.render(
|
|
100
|
+
ctx,
|
|
101
|
+
{
|
|
102
|
+
raw_values: await this.getSubformRawValues(ctx, sub_form_name),
|
|
103
|
+
messages: [],
|
|
104
|
+
},
|
|
105
|
+
show_field_errors
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async getSubformRawValues(
|
|
110
|
+
ctx: Context,
|
|
111
|
+
sub_form_name: string
|
|
112
|
+
): Promise<Record<string, FormDataValue>> {
|
|
113
|
+
const result = Object.fromEntries(
|
|
114
|
+
Object.entries(await this.extractRawValues(ctx))
|
|
115
|
+
.filter(([key]) =>
|
|
116
|
+
key.startsWith(sub_form_name + FIELD_PREFIX_SEPARATOR)
|
|
117
|
+
)
|
|
118
|
+
.map(([key, value]) => [
|
|
119
|
+
key.slice((sub_form_name + FIELD_PREFIX_SEPARATOR).length),
|
|
120
|
+
value,
|
|
121
|
+
])
|
|
122
|
+
);
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async render(
|
|
127
|
+
ctx: Context,
|
|
128
|
+
messages: FormMessage[],
|
|
129
|
+
prerenderedForms: Record<string, FlatTemplatable | undefined> = {},
|
|
130
|
+
show_field_errors: boolean
|
|
131
|
+
): Promise<FlatTemplatable> {
|
|
132
|
+
return tempstream/* HTML */ `${this.renderMessages(ctx, {
|
|
133
|
+
raw_values: {},
|
|
134
|
+
messages,
|
|
135
|
+
})}
|
|
136
|
+
<div class="forms">
|
|
137
|
+
<form
|
|
138
|
+
id="${this.name}"
|
|
139
|
+
novalidate
|
|
140
|
+
${
|
|
141
|
+
/* novalidate is here because all the subforms are actually one form and errors in one will prevent submitting all of the forms */ ""
|
|
142
|
+
}
|
|
143
|
+
method="POST"
|
|
144
|
+
></form>
|
|
145
|
+
${this.getSubformsToRender(ctx).map(([form_name]) => {
|
|
146
|
+
const frame_form_id =
|
|
147
|
+
this.name + "_" + form_name + "_frame_form";
|
|
148
|
+
return tempstream/* HTML */ `<turbo-frame
|
|
149
|
+
id="${form_name}"
|
|
150
|
+
contains-subform
|
|
151
|
+
subform-id="${frame_form_id}"
|
|
152
|
+
>
|
|
153
|
+
${prerenderedForms[form_name] ||
|
|
154
|
+
this.renderSubform(ctx, form_name, show_field_errors)}
|
|
155
|
+
<form id="${frame_form_id}" method="POST"></form>
|
|
156
|
+
</turbo-frame>`;
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
${this.makeBottomScript()}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
makeBottomScript(): string {
|
|
163
|
+
// this script assigns each field to its corresponding form, instead of
|
|
164
|
+
// the html-only version where all fields are attached to one meta-form
|
|
165
|
+
return /* HTML */ ` <script>
|
|
166
|
+
(function () {
|
|
167
|
+
if (!window.subform_handlers) {
|
|
168
|
+
window.subform_handlers = {};
|
|
169
|
+
}
|
|
170
|
+
if (window.subform_handlers["${this.name}"]) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const handler = () => {
|
|
174
|
+
document
|
|
175
|
+
.querySelectorAll("turbo-frame[contains-subform]")
|
|
176
|
+
.forEach((frame) => {
|
|
177
|
+
frame_form_id = frame.getAttribute("subform-id");
|
|
178
|
+
frame.querySelectorAll("input").forEach((input) => {
|
|
179
|
+
input.setAttribute("form", frame_form_id);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
window.subform_handlers["${this.name}"] = handler;
|
|
184
|
+
document.documentElement.addEventListener(
|
|
185
|
+
"turbo:load",
|
|
186
|
+
handler
|
|
187
|
+
);
|
|
188
|
+
document.documentElement.addEventListener(
|
|
189
|
+
"turbo:frame-render",
|
|
190
|
+
handler
|
|
191
|
+
);
|
|
192
|
+
})();
|
|
193
|
+
</script>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
mount(router: Router, path: string): void {
|
|
197
|
+
router.get(path, async (ctx) => {
|
|
198
|
+
ctx.type = "html";
|
|
199
|
+
ctx.body = await this.render(ctx, [], {}, false);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
for (const [key, form] of Object.entries(this.forms)) {
|
|
203
|
+
router.post(
|
|
204
|
+
path + (path.endsWith("/") ? "" : "/") + key,
|
|
205
|
+
async (ctx) => {
|
|
206
|
+
const result = await form.canAccess(ctx);
|
|
207
|
+
if (!result.canAccess) {
|
|
208
|
+
ctx.body = this.renderError(ctx, {
|
|
209
|
+
type: "access",
|
|
210
|
+
message: result.message,
|
|
211
|
+
});
|
|
212
|
+
ctx.status = 403;
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const reaction = await form.handlePost(ctx);
|
|
217
|
+
if (reaction.action == "stay") {
|
|
218
|
+
ctx.status = 422;
|
|
219
|
+
const form_content = reaction.content;
|
|
220
|
+
ctx.body = this.render(
|
|
221
|
+
ctx,
|
|
222
|
+
[],
|
|
223
|
+
{
|
|
224
|
+
[key]: form_content,
|
|
225
|
+
},
|
|
226
|
+
true
|
|
227
|
+
);
|
|
228
|
+
} else if (reaction.action == "redirect") {
|
|
229
|
+
ctx.status = 303;
|
|
230
|
+
ctx.redirect(reaction.url);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import _locreq from "locreq";
|
|
2
|
+
import * as prettier from "prettier";
|
|
3
|
+
import { promises as fs } from "fs";
|
|
4
|
+
import { assertType, predicates } from "@sealcode/ts-predicates";
|
|
5
|
+
import { getPrettierConfig } from "./utils/prettier";
|
|
6
|
+
|
|
7
|
+
const target_locreq = _locreq(process.cwd());
|
|
8
|
+
|
|
9
|
+
async function extractCollectionClassname(
|
|
10
|
+
full_file_path: string
|
|
11
|
+
): Promise<string> {
|
|
12
|
+
const file_content = await fs.readFile(full_file_path, "utf-8");
|
|
13
|
+
const result = /export default class (\w+)/.exec(file_content);
|
|
14
|
+
if (result === null) {
|
|
15
|
+
throw new Error("Missing 'export default class' statement?");
|
|
16
|
+
}
|
|
17
|
+
return assertType(
|
|
18
|
+
result[1],
|
|
19
|
+
predicates.string,
|
|
20
|
+
`Missing export default class in '${full_file_path}'?`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateCollections(): Promise<void> {
|
|
25
|
+
const collections_dir = target_locreq.resolve("src/back/collections");
|
|
26
|
+
const files = (await fs.readdir(collections_dir)).filter(
|
|
27
|
+
(f) =>
|
|
28
|
+
!/\.(sub)?test\./.exec(f) &&
|
|
29
|
+
f != "collections.ts" &&
|
|
30
|
+
f.endsWith(".ts")
|
|
31
|
+
);
|
|
32
|
+
const file_data = await Promise.all(
|
|
33
|
+
files.map(async (file) => {
|
|
34
|
+
const full_path = target_locreq.resolve(
|
|
35
|
+
"src/back/collections/" + file
|
|
36
|
+
);
|
|
37
|
+
const class_name = await extractCollectionClassname(full_path);
|
|
38
|
+
return {
|
|
39
|
+
class_name,
|
|
40
|
+
file,
|
|
41
|
+
full_path,
|
|
42
|
+
urlname: file.replace(".ts", ""),
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const content = `// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-collections'
|
|
48
|
+
import { App } from "sealious";
|
|
49
|
+
|
|
50
|
+
${file_data
|
|
51
|
+
.map(
|
|
52
|
+
({ class_name, urlname }) =>
|
|
53
|
+
`import _${class_name} from "./${urlname}";`
|
|
54
|
+
)
|
|
55
|
+
.join("\n")}
|
|
56
|
+
|
|
57
|
+
${file_data
|
|
58
|
+
.map(
|
|
59
|
+
({ class_name }) => `export const ${class_name} = new _${class_name}();`
|
|
60
|
+
)
|
|
61
|
+
.join("\n")}
|
|
62
|
+
|
|
63
|
+
export const collections = {
|
|
64
|
+
...App.BaseCollections,
|
|
65
|
+
${file_data
|
|
66
|
+
.map(({ class_name, urlname }) => ` "${urlname}": ${class_name},`)
|
|
67
|
+
.join("\n")}
|
|
68
|
+
};`;
|
|
69
|
+
await fs.writeFile(
|
|
70
|
+
target_locreq.resolve("src/back/collections/collections.ts"),
|
|
71
|
+
prettier.format(content, await getPrettierConfig())
|
|
72
|
+
);
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.log(
|
|
75
|
+
"Successfuly generated new src/back/collections/collections.ts file"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { promises as fs } from "fs";
|
|
2
|
+
import * as prettier from "prettier";
|
|
3
|
+
import { relative } from "path";
|
|
4
|
+
import _locreq from "locreq";
|
|
5
|
+
import { walkDir } from "./utils/walk";
|
|
6
|
+
import { importPath } from "./utils/import-path";
|
|
7
|
+
import { assertType, predicates } from "@sealcode/ts-predicates";
|
|
8
|
+
import { unescape_url_params } from "./utils/escape-url-params";
|
|
9
|
+
import { getPrettierConfig } from "./utils/prettier";
|
|
10
|
+
|
|
11
|
+
const target_locreq = _locreq(process.cwd());
|
|
12
|
+
|
|
13
|
+
async function extractActionName(full_file_path: string): Promise<string> {
|
|
14
|
+
const file_content = await fs.readFile(full_file_path, "utf-8");
|
|
15
|
+
const result = /export const actionName = "(\w+)"/.exec(file_content);
|
|
16
|
+
if (result === null) {
|
|
17
|
+
throw new Error("Missing 'export const actionName' statement?");
|
|
18
|
+
}
|
|
19
|
+
return assertType(
|
|
20
|
+
result[1],
|
|
21
|
+
predicates.string,
|
|
22
|
+
"Missing export default class?"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function generateURL(actionName: string, url: string) {
|
|
27
|
+
const parts = url.split("/");
|
|
28
|
+
const params = parts
|
|
29
|
+
.filter((part) => part.startsWith(":"))
|
|
30
|
+
.map((part) => part.slice(1));
|
|
31
|
+
if (params.length === 0) {
|
|
32
|
+
return `export const ${actionName}URL = "${url}";`;
|
|
33
|
+
}
|
|
34
|
+
return `export const ${actionName}URL = (${params
|
|
35
|
+
.map((s) => s + ": string")
|
|
36
|
+
.join(", ")})=>\`${parts
|
|
37
|
+
.map((part) =>
|
|
38
|
+
part.startsWith(":") ? "${" + part.slice(1) + "}" : part
|
|
39
|
+
)
|
|
40
|
+
.join("/")}\`;
|
|
41
|
+
${actionName}URL.params = [${params
|
|
42
|
+
.map((param) => `"${param}"`)
|
|
43
|
+
.join(",")}];
|
|
44
|
+
${actionName}URL.rawURL = "${url}";`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function generateRoutes(): Promise<void> {
|
|
48
|
+
const files = await Promise.all(
|
|
49
|
+
(
|
|
50
|
+
await walkDir(target_locreq.resolve("src/back/routes"))
|
|
51
|
+
)
|
|
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")
|
|
61
|
+
)
|
|
62
|
+
.map(async (fullpath) => ({
|
|
63
|
+
fullpath,
|
|
64
|
+
actionName: await extractActionName(fullpath),
|
|
65
|
+
url: unescape_url_params(
|
|
66
|
+
"/" +
|
|
67
|
+
relative(
|
|
68
|
+
target_locreq.resolve("src/back/routes"),
|
|
69
|
+
fullpath
|
|
70
|
+
).replace(/\..+/, "") +
|
|
71
|
+
"/" // trailing slash is important here, as it enables to use the entire path while building relative URLs. For example, while visiting /users/123, the path ./add-photo leads to /users/add-photo. While visiting /users/123/ (note the trailing slash), the path ./add-photo leads to /users/123/add-photo
|
|
72
|
+
),
|
|
73
|
+
}))
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const routes_content = `// DO NOT EDIT! This file is generated automaticaly with npm run generate-routes
|
|
77
|
+
|
|
78
|
+
import Router from "@koa/router";
|
|
79
|
+
import { mount } from "@sealcode/sealgen";
|
|
80
|
+
import * as URLs from "./urls"
|
|
81
|
+
|
|
82
|
+
${files
|
|
83
|
+
.map(
|
|
84
|
+
({ actionName, fullpath }) =>
|
|
85
|
+
`import { default as ${actionName} } from "${importPath(
|
|
86
|
+
target_locreq.resolve("src/back/routes/routes.ts"),
|
|
87
|
+
fullpath
|
|
88
|
+
)}";`
|
|
89
|
+
)
|
|
90
|
+
.join("\n")}
|
|
91
|
+
|
|
92
|
+
export default function mountAutoRoutes(router: Router) {
|
|
93
|
+
${files
|
|
94
|
+
.map(
|
|
95
|
+
({ actionName }) =>
|
|
96
|
+
` mount(router, URLs.${actionName}URL, ${actionName});`
|
|
97
|
+
)
|
|
98
|
+
.join("\n")}
|
|
99
|
+
}`;
|
|
100
|
+
|
|
101
|
+
const urls_content = `${files
|
|
102
|
+
.sort(({ url: url1 }, { url: url2 }) => (url1 < url2 ? -1 : 1))
|
|
103
|
+
.map(({ actionName, url }) => generateURL(actionName, url))
|
|
104
|
+
.join("\n")}
|
|
105
|
+
`;
|
|
106
|
+
|
|
107
|
+
const prettier_config = await getPrettierConfig();
|
|
108
|
+
await fs.writeFile(
|
|
109
|
+
target_locreq.resolve("src/back/routes/routes.ts"),
|
|
110
|
+
prettier.format(routes_content, prettier_config)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await fs.writeFile(
|
|
114
|
+
target_locreq.resolve("src/back/routes/urls.ts"),
|
|
115
|
+
prettier.format(urls_content, prettier_config)
|
|
116
|
+
);
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.log(
|
|
119
|
+
"Successfuly generated new src/back/routes/routes.ts and src/back/routes/urls.ts"
|
|
120
|
+
);
|
|
121
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import Sealious, { App } from "sealious";
|
|
2
|
+
import { FormDataValue } from "./forms/form";
|
|
3
|
+
|
|
4
|
+
declare module "koa" {
|
|
5
|
+
interface BaseContext {
|
|
6
|
+
$context: Sealious.Context;
|
|
7
|
+
$app: App;
|
|
8
|
+
$body: Record<string, FormDataValue>;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export * as Controls from "./forms/controls/controls";
|
|
13
|
+
export * as Fields from "./forms/fields/field";
|
|
14
|
+
export * from "./page/mountable";
|
|
15
|
+
export * from "./page/page";
|
|
16
|
+
export * from "./page/stateful-page";
|
|
17
|
+
export * from "./forms/form";
|
|
18
|
+
export * from "./page/props-parser";
|
|
19
|
+
export * from "./utils/utils";
|
|
20
|
+
export * from "./page/list";
|
|
21
|
+
export * from "./utils/translate";
|
|
22
|
+
export * as sanitize from "./sanitize";
|
|
23
|
+
export * from "./forms/multiform";
|
|
24
|
+
export * as Components from "./components/components";
|
|
25
|
+
export * from "./mount";
|
package/src/make-env.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
|
|
4
|
+
import _locreq from "locreq";
|
|
5
|
+
const target_locreq = _locreq(process.cwd());
|
|
6
|
+
|
|
7
|
+
export async function makeEnv(): Promise<void> {
|
|
8
|
+
const { uid, gid } = os.userInfo();
|
|
9
|
+
const content = `USER_ID=${uid}
|
|
10
|
+
GROUP_ID=${gid}
|
|
11
|
+
`;
|
|
12
|
+
const path = target_locreq.resolve(".env");
|
|
13
|
+
await fs.writeFile(path, content);
|
|
14
|
+
console.log("wrote .env");
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormDataValue } from ".";
|
|
4
|
+
import { FormField } from "./forms/fields/field";
|
|
5
|
+
import { attribute } from "./sanitize";
|
|
6
|
+
|
|
7
|
+
export function makeHiddenInputs(
|
|
8
|
+
ctx: Context,
|
|
9
|
+
fields: Record<string, FormField>,
|
|
10
|
+
values: Record<string, FormDataValue>,
|
|
11
|
+
fields_to_skip: string[]
|
|
12
|
+
): FlatTemplatable {
|
|
13
|
+
return tempstream`${Object.entries(fields)
|
|
14
|
+
.filter(([key]) => !fields_to_skip.includes(key))
|
|
15
|
+
.map(
|
|
16
|
+
([key, field]) => tempstream/* HTML */ `<input
|
|
17
|
+
type="hidden"
|
|
18
|
+
name="${key}"
|
|
19
|
+
value="${field
|
|
20
|
+
.getValue(ctx, values)
|
|
21
|
+
.then((r) => attribute(JSON.stringify(r.raw) || ""))}"
|
|
22
|
+
/>`
|
|
23
|
+
)}`;
|
|
24
|
+
}
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import Koa, { BaseContext, Context } from "koa";
|
|
3
|
+
import { Middlewares } from "sealious";
|
|
4
|
+
import { Mountable } from "./page/mountable";
|
|
5
|
+
|
|
6
|
+
async function handleHtmlPromise(ctx: Context, next: Koa.Next) {
|
|
7
|
+
await next();
|
|
8
|
+
if (ctx.body instanceof Promise) {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
10
|
+
ctx.body = await ctx.body;
|
|
11
|
+
}
|
|
12
|
+
ctx.set("content-type", "text/html;charset=utf-8");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function mount(
|
|
16
|
+
router: Router,
|
|
17
|
+
url: string | { rawURL: string },
|
|
18
|
+
mountable: Mountable,
|
|
19
|
+
use_dummy_app = false
|
|
20
|
+
): void {
|
|
21
|
+
const raw_url = typeof url === "string" ? url : url.rawURL;
|
|
22
|
+
const args = use_dummy_app
|
|
23
|
+
? [
|
|
24
|
+
async (ctx: BaseContext, next: any) => {
|
|
25
|
+
// this dummy is a temporary solution it's ilustrating the ways that
|
|
26
|
+
// sealgen is coupled with sealious. Definitely room for improvement in this
|
|
27
|
+
// regard
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
29
|
+
ctx.$app = {
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
31
|
+
Logger: new Proxy(
|
|
32
|
+
{},
|
|
33
|
+
{
|
|
34
|
+
get: (_target: any, prop: any) => {
|
|
35
|
+
if (prop === "info") {
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
37
|
+
return () => {};
|
|
38
|
+
} else {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
40
|
+
return () => {};
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
),
|
|
45
|
+
getString: (x: string) => x,
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
} as any;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
49
|
+
await next();
|
|
50
|
+
},
|
|
51
|
+
Middlewares.parseBody(),
|
|
52
|
+
handleHtmlPromise,
|
|
53
|
+
]
|
|
54
|
+
: [
|
|
55
|
+
Middlewares.extractContext(),
|
|
56
|
+
Middlewares.parseBody(),
|
|
57
|
+
handleHtmlPromise,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
router.use(raw_url, ...args);
|
|
61
|
+
mountable.init();
|
|
62
|
+
|
|
63
|
+
// to automatically add trailing slashes:
|
|
64
|
+
router.get(raw_url.slice(0, -1), async (ctx, next) => {
|
|
65
|
+
const url2 = ctx.URL;
|
|
66
|
+
if (!url2.pathname.endsWith("/")) {
|
|
67
|
+
url2.pathname += "/";
|
|
68
|
+
ctx.redirect(url2.href);
|
|
69
|
+
}
|
|
70
|
+
await next();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
router.use(raw_url, async (ctx, next) => {
|
|
74
|
+
ctx.set("content-type", "text/html;charset=utf-8");
|
|
75
|
+
const access_result = await mountable.canAccess(ctx);
|
|
76
|
+
if (!access_result.canAccess) {
|
|
77
|
+
ctx.status = 403;
|
|
78
|
+
ctx.body = access_result.message || "no access";
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await next();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
mountable.mount(router, raw_url);
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { ActionName, Collection } from "sealious";
|
|
3
|
+
|
|
4
|
+
export function peopleWhoCan(action: ActionName, collection: Collection) {
|
|
5
|
+
return async function (ctx: Context) {
|
|
6
|
+
const policy = collection.getPolicy(action);
|
|
7
|
+
const result = await policy.check(ctx.$context);
|
|
8
|
+
if (!result) {
|
|
9
|
+
ctx.status = 403;
|
|
10
|
+
return { canAccess: false, message: "Not allowed" };
|
|
11
|
+
}
|
|
12
|
+
if (!result.allowed) {
|
|
13
|
+
ctx.status = 403;
|
|
14
|
+
return { canAccess: false, message: result.reason };
|
|
15
|
+
}
|
|
16
|
+
return { canAccess: true, message: "" };
|
|
17
|
+
};
|
|
18
|
+
}
|