@sealcode/sealgen 0.2.1 → 0.3.2
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/.eslintrc.js +1 -0
- package/@types/build.d.ts +1 -0
- package/@types/cli.d.ts +2 -0
- package/@types/forms/controls/checkboxed-list-input.d.ts +16 -0
- package/@types/forms/controls/controls.d.ts +9 -0
- package/@types/forms/controls/dropdown.d.ts +20 -0
- package/@types/forms/controls/editable-collection-subset.d.ts +21 -0
- package/@types/forms/controls/form-control.d.ts +8 -0
- package/@types/forms/controls/form-field-control.d.ts +13 -0
- package/@types/forms/controls/form-header.d.ts +9 -0
- package/@types/forms/controls/form-paragraph.d.ts +7 -0
- package/@types/forms/controls/frame.d.ts +17 -0
- package/@types/forms/controls/simple-input.d.ts +28 -0
- package/@types/forms/controls.d.ts +0 -0
- package/@types/forms/field.d.ts +42 -0
- package/@types/forms/form-fields-list.d.ts +4 -0
- package/@types/forms/form.d.ts +42 -0
- package/@types/forms/validator.d.ts +3 -0
- package/@types/index.d.ts +15 -2
- package/@types/page/access-control.d.ts +6 -0
- package/@types/page/list.d.ts +59 -0
- package/@types/page/page.d.ts +26 -0
- package/@types/page/props-parser.d.ts +20 -0
- package/@types/run.d.ts +0 -0
- package/@types/templates/page.d.ts +1 -1
- package/@types/utils/natural-numbers.d.ts +1 -0
- package/@types/utils/sleep.d.ts +1 -0
- package/@types/utils/url-with-new-params.d.ts +2 -0
- package/@types/utils/utils.d.ts +9 -0
- package/@types/utils/walk.d.ts +1 -1
- package/lib/build.js +116 -0
- package/lib/build.js.map +1 -0
- package/lib/cli.js +30 -0
- package/lib/cli.js.map +1 -0
- package/lib/find-scss-includes.js +2 -1
- package/lib/find-scss-includes.js.map +1 -1
- package/lib/forms/controls/checkboxed-list-input.js +43 -0
- package/lib/forms/controls/checkboxed-list-input.js.map +1 -0
- package/lib/forms/controls/controls.js +22 -0
- package/lib/forms/controls/controls.js.map +1 -0
- package/lib/forms/controls/dropdown.js +40 -0
- package/lib/forms/controls/dropdown.js.map +1 -0
- package/lib/forms/controls/editable-collection-subset.js +73 -0
- package/lib/forms/controls/editable-collection-subset.js.map +1 -0
- package/lib/forms/controls/form-control.js +7 -0
- package/lib/forms/controls/form-control.js.map +1 -0
- package/lib/forms/controls/form-field-control.js +22 -0
- package/lib/forms/controls/form-field-control.js.map +1 -0
- package/lib/forms/controls/form-header.js +18 -0
- package/lib/forms/controls/form-header.js.map +1 -0
- package/lib/forms/controls/form-paragraph.js +16 -0
- package/lib/forms/controls/form-paragraph.js.map +1 -0
- package/lib/forms/controls/frame.js +29 -0
- package/lib/forms/controls/frame.js.map +1 -0
- package/lib/forms/controls/simple-input.js +50 -0
- package/lib/forms/controls/simple-input.js.map +1 -0
- package/lib/forms/controls.js +1 -0
- package/lib/forms/controls.js.map +1 -0
- package/lib/forms/field.js +80 -0
- package/lib/forms/field.js.map +1 -0
- package/lib/forms/form-fields-list.js +10 -0
- package/lib/forms/form-fields-list.js.map +1 -0
- package/lib/forms/form.js +162 -0
- package/lib/forms/form.js.map +1 -0
- package/lib/forms/validator.js +11 -0
- package/lib/forms/validator.js.map +1 -0
- package/lib/generate-routes.js +11 -4
- package/lib/generate-routes.js.map +1 -1
- package/lib/index.js +18 -25
- package/lib/index.js.map +1 -1
- package/lib/page/access-control.js +20 -0
- package/lib/page/access-control.js.map +1 -0
- package/lib/page/list.js +148 -0
- package/lib/page/list.js.map +1 -0
- package/lib/page/page.js +20 -0
- package/lib/page/page.js.map +1 -0
- package/lib/page/props-parser.js +74 -0
- package/lib/page/props-parser.js.map +1 -0
- package/lib/run.js +1 -0
- package/lib/run.js.map +1 -0
- package/lib/templates/form.js +10 -5
- package/lib/templates/form.js.map +1 -1
- package/lib/templates/list.js +3 -3
- package/lib/templates/list.js.map +1 -1
- package/lib/templates/page.js +5 -4
- package/lib/templates/page.js.map +1 -1
- package/lib/utils/escape-url-params.js +1 -0
- package/lib/utils/escape-url-params.js.map +1 -1
- package/lib/utils/list-collections.js +1 -1
- package/lib/utils/list-collections.js.map +1 -1
- package/lib/utils/natural-numbers.js +10 -0
- package/lib/utils/natural-numbers.js.map +1 -0
- package/lib/utils/sleep.js +8 -0
- package/lib/utils/sleep.js.map +1 -0
- package/lib/utils/url-with-new-params.js +12 -0
- package/lib/utils/url-with-new-params.js.map +1 -0
- package/lib/utils/utils.js +19 -0
- package/lib/utils/utils.js.map +1 -0
- package/lib/utils/walk.js +4 -4
- package/lib/utils/walk.js.map +1 -1
- package/package.json +17 -4
- package/src/build.ts +92 -0
- package/src/cli.ts +34 -0
- package/src/find-scss-includes.ts +3 -2
- package/src/forms/controls/checkboxed-list-input.ts +50 -0
- package/src/forms/controls/controls.ts +9 -0
- package/src/forms/controls/dropdown.ts +57 -0
- package/src/forms/controls/editable-collection-subset.ts +91 -0
- package/src/forms/controls/form-control.ts +13 -0
- package/src/forms/controls/form-field-control.ts +39 -0
- package/src/forms/controls/form-header.ts +16 -0
- package/src/forms/controls/form-paragraph.ts +11 -0
- package/src/forms/controls/frame.ts +26 -0
- package/src/forms/controls/simple-input.ts +73 -0
- package/src/forms/controls.ts +0 -0
- package/src/forms/field.ts +119 -0
- package/src/forms/form-fields-list.ts +7 -0
- package/src/forms/form.ts +242 -0
- package/src/forms/forms.scss +18 -0
- package/src/forms/validator.ts +10 -0
- package/src/generate-routes.ts +12 -9
- package/src/index.ts +15 -28
- package/src/page/access-control.ts +18 -0
- package/src/page/list.ts +214 -0
- package/src/page/page.ts +31 -0
- package/src/page/props-parser.ts +99 -0
- package/src/run.ts +0 -0
- package/src/templates/form.ts +10 -5
- package/src/templates/list.ts +3 -3
- package/src/templates/page.ts +6 -6
- package/src/utils/escape-url-params.ts +1 -0
- package/src/utils/list-collections.ts +2 -2
- package/src/utils/natural-numbers.ts +5 -0
- package/src/utils/sleep.ts +3 -0
- package/src/utils/url-with-new-params.ts +9 -0
- package/src/utils/utils.ts +10 -0
- package/src/utils/walk.ts +2 -2
- package/tsconfig.json +5 -2
- package/.log/ti-101379.log +0 -524
- package/.log/ti-131883.log +0 -505
- package/.log/ti-13490.log +0 -652
- package/.log/ti-138140.log +0 -505
- package/.log/ti-138288.log +0 -526
- package/.log/ti-46589.log +0 -675
- package/.log/tsserver.log +0 -1666
- package/src/tsconfig.json +0 -18
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { FormControl } from "./controls";
|
|
3
|
+
|
|
4
|
+
export class FormHeader extends FormControl {
|
|
5
|
+
role = <const>"decoration";
|
|
6
|
+
constructor(
|
|
7
|
+
public text: string,
|
|
8
|
+
public isVisible: (ctx: BaseContext) => Promise<boolean> = async () => true
|
|
9
|
+
) {
|
|
10
|
+
super();
|
|
11
|
+
}
|
|
12
|
+
async render(ctx: BaseContext) {
|
|
13
|
+
const isVsbl = await this.isVisible(ctx);
|
|
14
|
+
return isVsbl ? `<h2>${this.text}</h2>` : "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Templatable } from "tempstream";
|
|
2
|
+
import { FormControl } from "./controls";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* This class will render `turbo-frame` tag so that u can
|
|
6
|
+
* embed other route inside your form. This will require
|
|
7
|
+
* to add value to `data: FormData` (inside your master form
|
|
8
|
+
* render function) with key `Frame.FRAME_PATH_KEY`. Value
|
|
9
|
+
* needs to be url to route that you want to embed. If you
|
|
10
|
+
* this value wont be provided frame will redner empty string.
|
|
11
|
+
* See `src/back/routes/profile/[id].form.ts` for an example.
|
|
12
|
+
*/
|
|
13
|
+
export class Frame extends FormControl {
|
|
14
|
+
constructor(public src: string) {
|
|
15
|
+
super();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
render(): Templatable | Promise<Templatable> {
|
|
19
|
+
return /* HTML */ `<turbo-frame
|
|
20
|
+
id="contrahents"
|
|
21
|
+
loading="lazy"
|
|
22
|
+
src="${this.src}"
|
|
23
|
+
></turbo-frame>`;
|
|
24
|
+
}
|
|
25
|
+
role = <const>"decoration";
|
|
26
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { FormField } from "../field";
|
|
3
|
+
import { FormData } from "../form";
|
|
4
|
+
import { FormFieldsList } from "../form-fields-list";
|
|
5
|
+
import { FormFieldControl } from "./controls";
|
|
6
|
+
|
|
7
|
+
export class SimpleInput extends FormFieldControl {
|
|
8
|
+
constructor(
|
|
9
|
+
public fieldname: string,
|
|
10
|
+
public options: {
|
|
11
|
+
id?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
autocomplete?: boolean;
|
|
14
|
+
type?:
|
|
15
|
+
| "color"
|
|
16
|
+
| "date"
|
|
17
|
+
| "email"
|
|
18
|
+
| "file"
|
|
19
|
+
| "month"
|
|
20
|
+
| "number"
|
|
21
|
+
| "password"
|
|
22
|
+
| "search"
|
|
23
|
+
| "tel"
|
|
24
|
+
| "text"
|
|
25
|
+
| "time"
|
|
26
|
+
| "url"
|
|
27
|
+
| "week";
|
|
28
|
+
value?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
readonly?: boolean;
|
|
31
|
+
step?: number;
|
|
32
|
+
} = {}
|
|
33
|
+
) {
|
|
34
|
+
super([fieldname]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_render(_: BaseContext, fields: FormField[], data: FormData) {
|
|
38
|
+
const field = FormFieldsList.getField(fields, this.fieldname);
|
|
39
|
+
if (!field) {
|
|
40
|
+
throw new Error("wrong field name");
|
|
41
|
+
}
|
|
42
|
+
const id = this.options.id || field.name;
|
|
43
|
+
const label = this.options.label || field.name;
|
|
44
|
+
const type = this.options.type || "text";
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
46
|
+
const value = data.values[field.name] as string;
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
48
|
+
const raw_value = data.raw_values[field.name] as string;
|
|
49
|
+
const placeholder = this.options.placeholder || type;
|
|
50
|
+
const readonly = this.options.readonly || false;
|
|
51
|
+
const required = field.required;
|
|
52
|
+
const error = data.errors[field.name];
|
|
53
|
+
return /* HTML */ `<div class="input">
|
|
54
|
+
<label for="${id}">${label}</label>
|
|
55
|
+
<input
|
|
56
|
+
id="${id}"
|
|
57
|
+
type="${type}"
|
|
58
|
+
name="${field.name}"
|
|
59
|
+
value="${value === undefined
|
|
60
|
+
? raw_value == undefined
|
|
61
|
+
? ""
|
|
62
|
+
: raw_value
|
|
63
|
+
: value}"
|
|
64
|
+
placeholder="${placeholder}"
|
|
65
|
+
${readonly ? "readonly" : ""}
|
|
66
|
+
${required ? "required" : ""}
|
|
67
|
+
${!this.options.autocomplete ? `autocomplete="off"` : ""}
|
|
68
|
+
${this.options.step ? `step="${this.options.step}"` : ""}
|
|
69
|
+
/>
|
|
70
|
+
${error ? `<div class="input__error">${error}</div>` : ""}
|
|
71
|
+
</div>`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
2
|
+
import { BaseContext } from "koa";
|
|
3
|
+
|
|
4
|
+
export type FormFieldValidationResponse = { valid: boolean; message: string };
|
|
5
|
+
|
|
6
|
+
export type FormFieldValidationFn = (
|
|
7
|
+
ctx: BaseContext,
|
|
8
|
+
value: unknown,
|
|
9
|
+
field: FormField
|
|
10
|
+
) => Promise<FormFieldValidationResponse>;
|
|
11
|
+
|
|
12
|
+
export class FormField<Fieldnames extends string = string> {
|
|
13
|
+
constructor(
|
|
14
|
+
public name: Fieldnames,
|
|
15
|
+
public required: boolean = false,
|
|
16
|
+
public validator: FormFieldValidationFn = async () => ({
|
|
17
|
+
valid: true,
|
|
18
|
+
message: "",
|
|
19
|
+
})
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
public async _validate(
|
|
23
|
+
ctx: BaseContext,
|
|
24
|
+
value: unknown
|
|
25
|
+
): Promise<FormFieldValidationResponse> {
|
|
26
|
+
if (this.required && (value == "" || value == null || value == undefined)) {
|
|
27
|
+
return { valid: false, message: "This field is required" };
|
|
28
|
+
}
|
|
29
|
+
return this.validator(ctx, value, this);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public getEmptyValue() {
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class PickFromListField<
|
|
38
|
+
Fieldnames extends string = string
|
|
39
|
+
> extends FormField<Fieldnames> {
|
|
40
|
+
constructor(
|
|
41
|
+
public name: Fieldnames,
|
|
42
|
+
public required: boolean = false,
|
|
43
|
+
public generateOptions: (
|
|
44
|
+
ctx: BaseContext
|
|
45
|
+
) => Promise<Record<string, string> | { [i: string]: string }>,
|
|
46
|
+
public customValidation: (
|
|
47
|
+
ctx: BaseContext,
|
|
48
|
+
value: unknown,
|
|
49
|
+
instance: PickFromListField
|
|
50
|
+
) => Promise<FormFieldValidationResponse> = (ctx, value, instance) =>
|
|
51
|
+
instance.valueInList(ctx, value)
|
|
52
|
+
) {
|
|
53
|
+
super(name, required, (ctx, value) => this.customValidation(ctx, value, this));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async valueInList(
|
|
57
|
+
ctx: BaseContext,
|
|
58
|
+
value: unknown
|
|
59
|
+
): Promise<FormFieldValidationResponse> {
|
|
60
|
+
const options = await this.generateOptions(ctx);
|
|
61
|
+
if (!is(value, predicates.string)) {
|
|
62
|
+
return { valid: false, message: "not a string" };
|
|
63
|
+
}
|
|
64
|
+
if (!Object.keys(options).includes(value)) {
|
|
65
|
+
return { valid: false, message: `"${value}" is not one of the options` };
|
|
66
|
+
}
|
|
67
|
+
return { valid: true, message: "" };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class ChekboxedListField<
|
|
72
|
+
Fieldnames extends string = string
|
|
73
|
+
> extends FormField<Fieldnames> {
|
|
74
|
+
constructor(
|
|
75
|
+
public name: Fieldnames,
|
|
76
|
+
public required: boolean = false,
|
|
77
|
+
public generateOptions: (
|
|
78
|
+
ctx: BaseContext
|
|
79
|
+
) => Promise<Record<string, string> | { [i: string]: string }>,
|
|
80
|
+
public isVisible: (ctx: BaseContext) => Promise<boolean> = () =>
|
|
81
|
+
Promise.resolve(true)
|
|
82
|
+
) {
|
|
83
|
+
super(name, required, (ctx, value) => this.isValueValid(ctx, value));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async isValueValid(
|
|
87
|
+
_: BaseContext,
|
|
88
|
+
value: unknown
|
|
89
|
+
): Promise<FormFieldValidationResponse> {
|
|
90
|
+
if (is(value, predicates.string)) {
|
|
91
|
+
return { valid: false, message: "you need an array" };
|
|
92
|
+
}
|
|
93
|
+
if (is(value, predicates.null)) {
|
|
94
|
+
return { valid: false, message: "you need an array" };
|
|
95
|
+
}
|
|
96
|
+
return { valid: true, message: "" };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export class NumberField<
|
|
101
|
+
Fieldnames extends string = string
|
|
102
|
+
> extends FormField<Fieldnames> {
|
|
103
|
+
constructor(field_name: Fieldnames, required: boolean) {
|
|
104
|
+
super(field_name, required, (_, value) => this.isValueValid(_, value));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async isValueValid(_: BaseContext, value: unknown) {
|
|
108
|
+
if (
|
|
109
|
+
(is(value, predicates.string) &&
|
|
110
|
+
!isNaN(parseFloat(value)) &&
|
|
111
|
+
parseFloat(value).toString() == value.trim()) ||
|
|
112
|
+
is(value, predicates.number) ||
|
|
113
|
+
((is(value, predicates.undefined) || value == "") && !this.required)
|
|
114
|
+
) {
|
|
115
|
+
return { valid: true, message: "" };
|
|
116
|
+
}
|
|
117
|
+
return { valid: false, message: "Proszę wprowadzić liczbę" };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import Router from "@koa/router";
|
|
3
|
+
import { Templatable, tempstream } from "tempstream";
|
|
4
|
+
import { FormControl } from "./controls/controls";
|
|
5
|
+
import { hasShape, is, predicates } from "@sealcode/ts-predicates";
|
|
6
|
+
import { Mountable, PageErrorMessage } from "../page/page";
|
|
7
|
+
import { FormField } from "./field";
|
|
8
|
+
|
|
9
|
+
export type FormData<Fieldnames extends string = string> = {
|
|
10
|
+
values: Record<Fieldnames, string | string[] | number>;
|
|
11
|
+
raw_values: Record<Fieldnames, string | string[] | number>;
|
|
12
|
+
errors: Partial<Record<Fieldnames, string>>;
|
|
13
|
+
messages: { type: "info" | "success" | "error"; text: string }[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export abstract class Form<Fieldnames extends string = string>
|
|
17
|
+
implements Mountable
|
|
18
|
+
{
|
|
19
|
+
abstract fields: FormField<Fieldnames>[];
|
|
20
|
+
abstract controls: FormControl[];
|
|
21
|
+
defaultSuccessMessage = "Done";
|
|
22
|
+
submitButtonText = "Wyslij";
|
|
23
|
+
|
|
24
|
+
async canAccess(
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
26
|
+
_: BaseContext
|
|
27
|
+
): Promise<{ canAccess: boolean; message: string }> {
|
|
28
|
+
return { canAccess: true, message: "" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async renderError(
|
|
32
|
+
_: BaseContext,
|
|
33
|
+
error: PageErrorMessage
|
|
34
|
+
): Promise<Templatable> {
|
|
35
|
+
return tempstream/* HTML */ `<div>${error.message}</div>`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async validate(
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
40
|
+
_: BaseContext,
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
|
+
__: Record<string, unknown>
|
|
43
|
+
): Promise<{ valid: boolean; error: string }> {
|
|
44
|
+
return {
|
|
45
|
+
valid: true,
|
|
46
|
+
error: "",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private async _validate(
|
|
51
|
+
ctx: BaseContext,
|
|
52
|
+
values: Record<string, unknown>
|
|
53
|
+
): Promise<{
|
|
54
|
+
valid: boolean;
|
|
55
|
+
errors: Record<Fieldnames | "form", string>;
|
|
56
|
+
}> {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
58
|
+
const errors = {} as Record<Fieldnames | "form", string>;
|
|
59
|
+
let valid = true;
|
|
60
|
+
await Promise.all(
|
|
61
|
+
this.fields.map(async (field) => {
|
|
62
|
+
const { valid: fieldvalid, message: fieldmessage } =
|
|
63
|
+
await field._validate(ctx, values[field.name]);
|
|
64
|
+
if (!fieldvalid) {
|
|
65
|
+
valid = false;
|
|
66
|
+
errors[field.name] = fieldmessage;
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
const formValidationResult = await this.validate(ctx, values);
|
|
71
|
+
if (!formValidationResult.valid) {
|
|
72
|
+
valid = false;
|
|
73
|
+
errors.form = formValidationResult.error;
|
|
74
|
+
}
|
|
75
|
+
return { valid, errors };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async render(
|
|
79
|
+
ctx: BaseContext,
|
|
80
|
+
data: FormData,
|
|
81
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
82
|
+
_: string
|
|
83
|
+
): Promise<Templatable> {
|
|
84
|
+
return tempstream/* HTML */ `${this.makeFormTag(
|
|
85
|
+
`${ctx.URL.pathname}/`
|
|
86
|
+
)} ${
|
|
87
|
+
!this.controls.some((control) => control.role == "messages")
|
|
88
|
+
? this.renderMessages(ctx, data)
|
|
89
|
+
: ""
|
|
90
|
+
} ${
|
|
91
|
+
data.errors.form !== undefined
|
|
92
|
+
? `<div class="form__error">${data.errors.form}</div>`
|
|
93
|
+
: ""
|
|
94
|
+
} ${this.renderControls(ctx, data)}<input type="submit" value="${
|
|
95
|
+
this.submitButtonText
|
|
96
|
+
}"/></form>`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public renderMessages(_: BaseContext, data: FormData): Templatable {
|
|
100
|
+
return tempstream/* HTML */ `<div class="form-messages">
|
|
101
|
+
${data.messages.map(
|
|
102
|
+
(message) =>
|
|
103
|
+
`<div class="form-message form-message--${message.type}">${message.text}</div>`
|
|
104
|
+
)}
|
|
105
|
+
</div>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public renderControls(ctx: BaseContext, data: FormData): Templatable {
|
|
109
|
+
return tempstream/* HTML */ `${this.controls.map((control) =>
|
|
110
|
+
control.render(ctx, this.fields, data)
|
|
111
|
+
)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public makeFormTag(path: string) {
|
|
115
|
+
return `<form method="POST" action="${path}">`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private generateData(rawData: Record<string, unknown> = {}): FormData {
|
|
119
|
+
// generates a FormData object that has the correct shape to be passed to
|
|
120
|
+
// render(), so for example it makes sure that all fields either have values or
|
|
121
|
+
// are empty string (the aren't undefined, for example). If no argument is passed,
|
|
122
|
+
// creates an object that represents an empty state of the form. If some data
|
|
123
|
+
// object is passed in the first argument, then the values in that data object are
|
|
124
|
+
// incorporated into the generated object
|
|
125
|
+
|
|
126
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
127
|
+
const values = Object.fromEntries(
|
|
128
|
+
this.fields.map((f) => [
|
|
129
|
+
f.name,
|
|
130
|
+
rawData[f.name] || f.getEmptyValue(),
|
|
131
|
+
])
|
|
132
|
+
) as Record<Fieldnames, string>;
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
134
|
+
const errors = Object.fromEntries(
|
|
135
|
+
this.fields.map((f) => [f.name, ""])
|
|
136
|
+
) as Record<Fieldnames, string>;
|
|
137
|
+
return {
|
|
138
|
+
values,
|
|
139
|
+
errors,
|
|
140
|
+
messages: [],
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
142
|
+
raw_values: rawData as Record<string, string>,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public async onValuesInvalid(
|
|
147
|
+
ctx: BaseContext,
|
|
148
|
+
errors: Record<Fieldnames, string>,
|
|
149
|
+
form_path: string
|
|
150
|
+
) {
|
|
151
|
+
ctx.status = 422;
|
|
152
|
+
const { values, raw_values } = this.generateData(ctx.$body);
|
|
153
|
+
ctx.body = await this.render(
|
|
154
|
+
ctx,
|
|
155
|
+
{
|
|
156
|
+
values,
|
|
157
|
+
raw_values,
|
|
158
|
+
errors,
|
|
159
|
+
messages: [{ type: "error", text: "Some fields are invalid" }],
|
|
160
|
+
},
|
|
161
|
+
form_path
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public async onError(ctx: BaseContext, error: unknown, form_path: string) {
|
|
166
|
+
ctx.status = 422;
|
|
167
|
+
let error_message = "Unknown error has occured";
|
|
168
|
+
if (
|
|
169
|
+
is(error, predicates.object) &&
|
|
170
|
+
hasShape({ message: predicates.string }, error)
|
|
171
|
+
) {
|
|
172
|
+
error_message = error.message;
|
|
173
|
+
}
|
|
174
|
+
const { values, raw_values } = this.generateData(ctx.$body);
|
|
175
|
+
ctx.body = await this.render(
|
|
176
|
+
ctx,
|
|
177
|
+
{
|
|
178
|
+
values,
|
|
179
|
+
raw_values,
|
|
180
|
+
errors: {},
|
|
181
|
+
messages: [{ type: "error", text: error_message }],
|
|
182
|
+
},
|
|
183
|
+
form_path
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
public abstract onSubmit(
|
|
188
|
+
ctx: BaseContext,
|
|
189
|
+
values: Record<Fieldnames, string | string[] | number>
|
|
190
|
+
): void | Promise<void>;
|
|
191
|
+
|
|
192
|
+
public async onSuccess(ctx: BaseContext, form_path: string): Promise<void> {
|
|
193
|
+
const { values, raw_values } = this.generateData(ctx.$body);
|
|
194
|
+
ctx.body = await this.render(
|
|
195
|
+
ctx,
|
|
196
|
+
{
|
|
197
|
+
values,
|
|
198
|
+
raw_values,
|
|
199
|
+
errors: {},
|
|
200
|
+
messages: [
|
|
201
|
+
{ type: "success", text: this.defaultSuccessMessage },
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
form_path
|
|
205
|
+
);
|
|
206
|
+
ctx.status = 422;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
public mount(router: Router, path: string) {
|
|
210
|
+
router.use(path, async (ctx, next) => {
|
|
211
|
+
const result = await this.canAccess(ctx);
|
|
212
|
+
if (!result.canAccess) {
|
|
213
|
+
ctx.body = this.renderError(ctx, {
|
|
214
|
+
type: "access",
|
|
215
|
+
message: result.message,
|
|
216
|
+
});
|
|
217
|
+
ctx.status = 403;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
await next();
|
|
221
|
+
});
|
|
222
|
+
router.get(path, async (ctx) => {
|
|
223
|
+
ctx.type = "html";
|
|
224
|
+
ctx.body = await this.render(ctx, this.generateData(), path);
|
|
225
|
+
});
|
|
226
|
+
router.post(path, async (ctx) => {
|
|
227
|
+
const { valid, errors } = await this._validate(ctx, ctx.$body);
|
|
228
|
+
if (!valid) {
|
|
229
|
+
await this.onValuesInvalid(ctx, errors, path);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await this.onSubmit(ctx, this.generateData(ctx.$body).values);
|
|
234
|
+
await this.onSuccess(ctx, path);
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// eslint-disable-next-line no-console
|
|
237
|
+
console.dir(e, { depth: 5 });
|
|
238
|
+
await this.onError(ctx, e, path);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { Field } from "sealious";
|
|
3
|
+
import { FormFieldValidationFn } from "./field";
|
|
4
|
+
|
|
5
|
+
export function collectionFieldValidator(field: Field): FormFieldValidationFn {
|
|
6
|
+
return async (ctx: BaseContext, value) => {
|
|
7
|
+
const { valid, reason } = await field.checkValue(ctx.$context, value, undefined);
|
|
8
|
+
return { valid, message: reason || (valid ? "Wrong value" : "") };
|
|
9
|
+
};
|
|
10
|
+
}
|
package/src/generate-routes.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from "fs";
|
|
2
2
|
import { relative } from "path";
|
|
3
3
|
import _locreq from "locreq";
|
|
4
|
-
import {
|
|
4
|
+
import { walkDir } from "./utils/walk";
|
|
5
5
|
import { importPath } from "./utils/import-path";
|
|
6
6
|
import { assertType, predicates } from "@sealcode/ts-predicates";
|
|
7
7
|
import { unescape_url_params } from "./utils/escape-url-params";
|
|
@@ -24,7 +24,7 @@ async function extractActionName(full_file_path: string): Promise<string> {
|
|
|
24
24
|
export async function generateRoutes(): Promise<void> {
|
|
25
25
|
const files = await Promise.all(
|
|
26
26
|
(
|
|
27
|
-
await
|
|
27
|
+
await walkDir(target_locreq.resolve("src/back/routes"))
|
|
28
28
|
)
|
|
29
29
|
.filter(
|
|
30
30
|
(f) =>
|
|
@@ -52,6 +52,7 @@ export async function generateRoutes(): Promise<void> {
|
|
|
52
52
|
|
|
53
53
|
import Router from "@koa/router";
|
|
54
54
|
import { Middlewares } from "sealious";
|
|
55
|
+
import { Mountable } from "@sealcode/sealgen";
|
|
55
56
|
|
|
56
57
|
${files
|
|
57
58
|
.map(
|
|
@@ -67,15 +68,17 @@ ${files
|
|
|
67
68
|
.map(({ actionName, url }) => `export const ${actionName}URL = "${url}";`)
|
|
68
69
|
.join("\n")}
|
|
69
70
|
|
|
71
|
+
function mount(router: Router, URL: string, mountable: Mountable){
|
|
72
|
+
router.use(URL, Middlewares.extractContext(), Middlewares.parseBody());
|
|
73
|
+
mountable.mount(router, URL);
|
|
74
|
+
// to automatically add trailing slashes:
|
|
75
|
+
router.get(URL.slice(0, -1), (ctx) => ctx.redirect(URL));
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
export default function mountAutoRoutes(router: Router) {
|
|
71
79
|
${files
|
|
72
|
-
.map(
|
|
73
|
-
|
|
74
|
-
actionName,
|
|
75
|
-
}) => ` router.use(${actionName}URL, Middlewares.extractContext(), Middlewares.parseBody());
|
|
76
|
-
${actionName}.mount(router, ${actionName}URL);`
|
|
77
|
-
)
|
|
78
|
-
.join("\n\n")}
|
|
80
|
+
.map(({ actionName }) => ` mount(router, ${actionName}URL, ${actionName});`)
|
|
81
|
+
.join("\n")}
|
|
79
82
|
}
|
|
80
83
|
`
|
|
81
84
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import Sealious from "sealious";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
declare module "koa" {
|
|
4
|
+
interface BaseContext {
|
|
5
|
+
$context: Sealious.Context;
|
|
6
|
+
$body: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
console.log(
|
|
18
|
-
`Available actions: ${Object.keys(actions)
|
|
19
|
-
.filter((e) => e != "default")
|
|
20
|
-
.join(", ")}`
|
|
21
|
-
);
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
void (async function () {
|
|
26
|
-
const action = process.argv.at(-1);
|
|
27
|
-
actions;
|
|
28
|
-
const fn = actions[action || "default"] || actions.default;
|
|
29
|
-
await fn();
|
|
30
|
-
})();
|
|
10
|
+
export * from "./forms/form";
|
|
11
|
+
export * from "./forms/controls/controls";
|
|
12
|
+
export * from "./forms/field";
|
|
13
|
+
export * from "./page/page";
|
|
14
|
+
export * from "./page/props-parser";
|
|
15
|
+
export * from "./utils/utils";
|
|
16
|
+
export * from "./forms/validator";
|
|
17
|
+
export * from "./page/list";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { ActionName, Collection } from "sealious";
|
|
3
|
+
|
|
4
|
+
export function peopleWhoCan(action: ActionName, collection: Collection) {
|
|
5
|
+
return async function (ctx: BaseContext) {
|
|
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
|
+
}
|