@sealcode/sealgen 0.2.1 → 0.2.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/.log/ti-292609.log +508 -0
- package/.log/ti-304933.log +524 -0
- package/.log/ti-312312.log +508 -0
- package/.log/ti-314240.log +1617 -0
- package/.log/ti-62627.log +508 -0
- package/.log/ti-64785.log +508 -0
- package/.log/ti-65911.log +533 -0
- package/.log/ti-69885.log +548 -0
- package/.log/ti-72106.log +551 -0
- package/.log/ti-74858.log +508 -0
- package/.log/tsserver.log +1610 -1625
- 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 +14 -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/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 +108 -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 +1 -1
- package/lib/generate-routes.js.map +1 -1
- package/lib/index.js +17 -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/list.js +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 +82 -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 +2 -2
- package/src/index.ts +14 -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/list.ts +1 -1
- 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/src/tsconfig.json +0 -18
|
@@ -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) =>
|
package/src/index.ts
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
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
|
-
console.log("Usage: `sealscript <action>`");
|
|
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";
|
|
@@ -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
|
+
}
|
package/src/page/list.ts
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { Collection, CollectionItem } from "sealious";
|
|
3
|
+
import { Templatable, tempstream } from "tempstream";
|
|
4
|
+
import { peopleWhoCan } from "./access-control";
|
|
5
|
+
import { Page } from "./page";
|
|
6
|
+
import { predicates, ShapeToType } from "@sealcode/ts-predicates";
|
|
7
|
+
import { PagePropsParser } from "./props-parser";
|
|
8
|
+
import { FormFieldControl } from "../forms/controls/controls";
|
|
9
|
+
import { FormField } from "../forms/field";
|
|
10
|
+
import { FormData } from "../forms/form";
|
|
11
|
+
import { FormFieldsList } from "../forms/form-fields-list";
|
|
12
|
+
import { naturalNumbers, UrlWithNewParams } from "../utils/utils";
|
|
13
|
+
|
|
14
|
+
export const BasePagePropsShape = <const>{};
|
|
15
|
+
export type BasePageProps = ShapeToType<typeof BasePagePropsShape>;
|
|
16
|
+
|
|
17
|
+
export const BaseListPagePropsShape = <const>{
|
|
18
|
+
page: predicates.number,
|
|
19
|
+
itemsPerPage: predicates.number,
|
|
20
|
+
};
|
|
21
|
+
export type BaseListPageProps = ShapeToType<typeof BaseListPagePropsShape>;
|
|
22
|
+
export const BaseListPageDefaultProps = { page: 1, itemsPerPage: 25 };
|
|
23
|
+
|
|
24
|
+
export type PropsErrors<PropsType> = Partial<Record<keyof PropsType, string>>;
|
|
25
|
+
|
|
26
|
+
export abstract class ListPage<
|
|
27
|
+
ItemType,
|
|
28
|
+
PropsType extends BaseListPageProps = BaseListPageProps
|
|
29
|
+
> extends Page {
|
|
30
|
+
abstract getItems(
|
|
31
|
+
ctx: BaseContext,
|
|
32
|
+
props: PropsType
|
|
33
|
+
): Promise<{ items: ItemType[] }>;
|
|
34
|
+
abstract getTotalPages(ctx: BaseContext, props: PropsType): Promise<number>;
|
|
35
|
+
abstract renderItem(ctx: BaseContext, item: ItemType): Promise<Templatable>;
|
|
36
|
+
abstract propsParser: PagePropsParser<PropsType>;
|
|
37
|
+
|
|
38
|
+
filterFields: FormField<keyof PropsType>[] = [];
|
|
39
|
+
filterControls: FormFieldControl[] = [];
|
|
40
|
+
|
|
41
|
+
renderListContainer(_: BaseContext, content: Templatable): Templatable {
|
|
42
|
+
return tempstream`<div>${content}</div>`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async validateProps(
|
|
46
|
+
ctx: BaseContext,
|
|
47
|
+
props: PropsType
|
|
48
|
+
): Promise<{ valid: boolean; errors: PropsErrors<PropsType> }> {
|
|
49
|
+
const errors: PropsErrors<PropsType> = {};
|
|
50
|
+
let has_errors = false;
|
|
51
|
+
const promises = [];
|
|
52
|
+
for (const [key, value] of Object.entries(props)) {
|
|
53
|
+
const field = FormFieldsList.getField(this.filterFields, key);
|
|
54
|
+
if (field) {
|
|
55
|
+
promises.push(
|
|
56
|
+
field._validate(ctx, value).then(({ valid, message }) => {
|
|
57
|
+
if (!valid) {
|
|
58
|
+
has_errors = true;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
60
|
+
errors[key as keyof PropsType] = message;
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await Promise.all(promises);
|
|
67
|
+
return { valid: has_errors, errors };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async getProps(ctx: BaseContext): Promise<{
|
|
71
|
+
parsed_props: PropsType;
|
|
72
|
+
errors: PropsErrors<PropsType>;
|
|
73
|
+
raw_props: PropsType;
|
|
74
|
+
}> {
|
|
75
|
+
const raw_props = this.propsParser.decode(ctx);
|
|
76
|
+
const parsed_props = { ...raw_props };
|
|
77
|
+
const { errors } = await this.validateProps(ctx, parsed_props);
|
|
78
|
+
for (const prop_name in errors) {
|
|
79
|
+
const default_value = this.propsParser.getDefaultValue(prop_name);
|
|
80
|
+
if (default_value !== undefined) {
|
|
81
|
+
parsed_props[prop_name] = default_value;
|
|
82
|
+
} else {
|
|
83
|
+
delete parsed_props[prop_name];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { parsed_props, errors, raw_props };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async render(ctx: BaseContext) {
|
|
90
|
+
const { parsed_props, errors, raw_props } = await this.getProps(ctx);
|
|
91
|
+
|
|
92
|
+
return tempstream`${this.renderPagination(ctx, parsed_props)}
|
|
93
|
+
${this.renderFilters(ctx, parsed_props, raw_props, errors)}
|
|
94
|
+
${this.getItems(ctx, parsed_props).then(({ items }) =>
|
|
95
|
+
this.renderListContainer(
|
|
96
|
+
ctx,
|
|
97
|
+
items.map((item) => this.renderItem(ctx, item))
|
|
98
|
+
)
|
|
99
|
+
)}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async renderPagination(ctx: BaseContext, props: PropsType) {
|
|
103
|
+
const totalIems = await this.getTotalPages(ctx, props);
|
|
104
|
+
const currentPage = props.page;
|
|
105
|
+
|
|
106
|
+
return tempstream/* HTML */ `<center>
|
|
107
|
+
${currentPage > 1
|
|
108
|
+
? this.renderPageButton(ctx, 1, "Pierwsza strona")
|
|
109
|
+
: ""}
|
|
110
|
+
${currentPage > 1
|
|
111
|
+
? this.renderPageButton(
|
|
112
|
+
ctx,
|
|
113
|
+
currentPage - 1,
|
|
114
|
+
"Poprzednia strona"
|
|
115
|
+
)
|
|
116
|
+
: ""}
|
|
117
|
+
|
|
118
|
+
<select onchange="if (this.value) Turbo.visit(this.value)">
|
|
119
|
+
${Array.from(
|
|
120
|
+
naturalNumbers(1, await this.getTotalPages(ctx, props))
|
|
121
|
+
).map(
|
|
122
|
+
(n) => /* HTML */ `<option
|
|
123
|
+
value="${UrlWithNewParams(
|
|
124
|
+
ctx,
|
|
125
|
+
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
126
|
+
this.propsParser.overwriteProp(ctx, {
|
|
127
|
+
page: n,
|
|
128
|
+
} as Partial<PropsType>)
|
|
129
|
+
)}"
|
|
130
|
+
${currentPage === n ? "selected" : ""}
|
|
131
|
+
>
|
|
132
|
+
${n}
|
|
133
|
+
</option>`
|
|
134
|
+
)}
|
|
135
|
+
</select>
|
|
136
|
+
${currentPage < totalIems
|
|
137
|
+
? this.renderPageButton(ctx, currentPage + 1, "Następna strona")
|
|
138
|
+
: ""}
|
|
139
|
+
${currentPage < totalIems
|
|
140
|
+
? this.renderPageButton(ctx, totalIems, "Ostatnia strona")
|
|
141
|
+
: ""}
|
|
142
|
+
</center>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private renderPageButton(ctx: BaseContext, page: number, text: string) {
|
|
146
|
+
return /* HTML */ `<a
|
|
147
|
+
href="${UrlWithNewParams(
|
|
148
|
+
ctx,
|
|
149
|
+
//eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
150
|
+
this.propsParser.overwriteProp(ctx, {
|
|
151
|
+
page,
|
|
152
|
+
} as Partial<PropsType>)
|
|
153
|
+
)}"
|
|
154
|
+
>${text}</a
|
|
155
|
+
>`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
renderFilters(
|
|
159
|
+
ctx: BaseContext,
|
|
160
|
+
parsed_props: PropsType, // parsed props don't include wrong values
|
|
161
|
+
raw_props: PropsType,
|
|
162
|
+
errors: PropsErrors<PropsType>
|
|
163
|
+
) {
|
|
164
|
+
return tempstream/* HTML */ `<form method="GET">
|
|
165
|
+
${this.propsParser.makeHiddenInputs(parsed_props, [
|
|
166
|
+
"page",
|
|
167
|
+
...this.filterFields.map((f) => f.name),
|
|
168
|
+
])}
|
|
169
|
+
${this.filterControls.map((control) =>
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
171
|
+
control.render(ctx, this.filterFields, {
|
|
172
|
+
values: parsed_props,
|
|
173
|
+
raw_values: raw_props,
|
|
174
|
+
errors,
|
|
175
|
+
messages: [],
|
|
176
|
+
} as FormData)
|
|
177
|
+
)}
|
|
178
|
+
</form>`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export abstract class SealiousItemListPage<
|
|
183
|
+
C extends Collection,
|
|
184
|
+
PageProps extends BaseListPageProps = BaseListPageProps
|
|
185
|
+
> extends ListPage<CollectionItem<C>, PageProps> {
|
|
186
|
+
constructor(public collection: C) {
|
|
187
|
+
super();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getTotalPages(ctx: BaseContext, props: PageProps) {
|
|
191
|
+
const { items } = await this.collection.list(ctx.$context).fetch();
|
|
192
|
+
return Math.ceil(items.length / props.itemsPerPage);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async getItems(ctx: BaseContext, props: PageProps) {
|
|
196
|
+
return {
|
|
197
|
+
items: (
|
|
198
|
+
await this.collection
|
|
199
|
+
.list(ctx.$context)
|
|
200
|
+
.paginate({ items: props.itemsPerPage, page: props.page })
|
|
201
|
+
.fetch()
|
|
202
|
+
).items,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async renderItem(
|
|
207
|
+
_: BaseContext,
|
|
208
|
+
item: CollectionItem<C>
|
|
209
|
+
): Promise<Templatable> {
|
|
210
|
+
return `<div>${item.id}</div>`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
canAccess = peopleWhoCan("list", this.collection);
|
|
214
|
+
}
|