@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.
Files changed (146) hide show
  1. package/.eslintrc.js +1 -0
  2. package/@types/build.d.ts +1 -0
  3. package/@types/cli.d.ts +2 -0
  4. package/@types/forms/controls/checkboxed-list-input.d.ts +16 -0
  5. package/@types/forms/controls/controls.d.ts +9 -0
  6. package/@types/forms/controls/dropdown.d.ts +20 -0
  7. package/@types/forms/controls/editable-collection-subset.d.ts +21 -0
  8. package/@types/forms/controls/form-control.d.ts +8 -0
  9. package/@types/forms/controls/form-field-control.d.ts +13 -0
  10. package/@types/forms/controls/form-header.d.ts +9 -0
  11. package/@types/forms/controls/form-paragraph.d.ts +7 -0
  12. package/@types/forms/controls/frame.d.ts +17 -0
  13. package/@types/forms/controls/simple-input.d.ts +28 -0
  14. package/@types/forms/controls.d.ts +0 -0
  15. package/@types/forms/field.d.ts +42 -0
  16. package/@types/forms/form-fields-list.d.ts +4 -0
  17. package/@types/forms/form.d.ts +42 -0
  18. package/@types/forms/validator.d.ts +3 -0
  19. package/@types/index.d.ts +15 -2
  20. package/@types/page/access-control.d.ts +6 -0
  21. package/@types/page/list.d.ts +59 -0
  22. package/@types/page/page.d.ts +26 -0
  23. package/@types/page/props-parser.d.ts +20 -0
  24. package/@types/run.d.ts +0 -0
  25. package/@types/templates/page.d.ts +1 -1
  26. package/@types/utils/natural-numbers.d.ts +1 -0
  27. package/@types/utils/sleep.d.ts +1 -0
  28. package/@types/utils/url-with-new-params.d.ts +2 -0
  29. package/@types/utils/utils.d.ts +9 -0
  30. package/@types/utils/walk.d.ts +1 -1
  31. package/lib/build.js +116 -0
  32. package/lib/build.js.map +1 -0
  33. package/lib/cli.js +30 -0
  34. package/lib/cli.js.map +1 -0
  35. package/lib/find-scss-includes.js +2 -1
  36. package/lib/find-scss-includes.js.map +1 -1
  37. package/lib/forms/controls/checkboxed-list-input.js +43 -0
  38. package/lib/forms/controls/checkboxed-list-input.js.map +1 -0
  39. package/lib/forms/controls/controls.js +22 -0
  40. package/lib/forms/controls/controls.js.map +1 -0
  41. package/lib/forms/controls/dropdown.js +40 -0
  42. package/lib/forms/controls/dropdown.js.map +1 -0
  43. package/lib/forms/controls/editable-collection-subset.js +73 -0
  44. package/lib/forms/controls/editable-collection-subset.js.map +1 -0
  45. package/lib/forms/controls/form-control.js +7 -0
  46. package/lib/forms/controls/form-control.js.map +1 -0
  47. package/lib/forms/controls/form-field-control.js +22 -0
  48. package/lib/forms/controls/form-field-control.js.map +1 -0
  49. package/lib/forms/controls/form-header.js +18 -0
  50. package/lib/forms/controls/form-header.js.map +1 -0
  51. package/lib/forms/controls/form-paragraph.js +16 -0
  52. package/lib/forms/controls/form-paragraph.js.map +1 -0
  53. package/lib/forms/controls/frame.js +29 -0
  54. package/lib/forms/controls/frame.js.map +1 -0
  55. package/lib/forms/controls/simple-input.js +50 -0
  56. package/lib/forms/controls/simple-input.js.map +1 -0
  57. package/lib/forms/controls.js +1 -0
  58. package/lib/forms/controls.js.map +1 -0
  59. package/lib/forms/field.js +80 -0
  60. package/lib/forms/field.js.map +1 -0
  61. package/lib/forms/form-fields-list.js +10 -0
  62. package/lib/forms/form-fields-list.js.map +1 -0
  63. package/lib/forms/form.js +162 -0
  64. package/lib/forms/form.js.map +1 -0
  65. package/lib/forms/validator.js +11 -0
  66. package/lib/forms/validator.js.map +1 -0
  67. package/lib/generate-routes.js +11 -4
  68. package/lib/generate-routes.js.map +1 -1
  69. package/lib/index.js +18 -25
  70. package/lib/index.js.map +1 -1
  71. package/lib/page/access-control.js +20 -0
  72. package/lib/page/access-control.js.map +1 -0
  73. package/lib/page/list.js +148 -0
  74. package/lib/page/list.js.map +1 -0
  75. package/lib/page/page.js +20 -0
  76. package/lib/page/page.js.map +1 -0
  77. package/lib/page/props-parser.js +74 -0
  78. package/lib/page/props-parser.js.map +1 -0
  79. package/lib/run.js +1 -0
  80. package/lib/run.js.map +1 -0
  81. package/lib/templates/form.js +10 -5
  82. package/lib/templates/form.js.map +1 -1
  83. package/lib/templates/list.js +3 -3
  84. package/lib/templates/list.js.map +1 -1
  85. package/lib/templates/page.js +5 -4
  86. package/lib/templates/page.js.map +1 -1
  87. package/lib/utils/escape-url-params.js +1 -0
  88. package/lib/utils/escape-url-params.js.map +1 -1
  89. package/lib/utils/list-collections.js +1 -1
  90. package/lib/utils/list-collections.js.map +1 -1
  91. package/lib/utils/natural-numbers.js +10 -0
  92. package/lib/utils/natural-numbers.js.map +1 -0
  93. package/lib/utils/sleep.js +8 -0
  94. package/lib/utils/sleep.js.map +1 -0
  95. package/lib/utils/url-with-new-params.js +12 -0
  96. package/lib/utils/url-with-new-params.js.map +1 -0
  97. package/lib/utils/utils.js +19 -0
  98. package/lib/utils/utils.js.map +1 -0
  99. package/lib/utils/walk.js +4 -4
  100. package/lib/utils/walk.js.map +1 -1
  101. package/package.json +17 -4
  102. package/src/build.ts +92 -0
  103. package/src/cli.ts +34 -0
  104. package/src/find-scss-includes.ts +3 -2
  105. package/src/forms/controls/checkboxed-list-input.ts +50 -0
  106. package/src/forms/controls/controls.ts +9 -0
  107. package/src/forms/controls/dropdown.ts +57 -0
  108. package/src/forms/controls/editable-collection-subset.ts +91 -0
  109. package/src/forms/controls/form-control.ts +13 -0
  110. package/src/forms/controls/form-field-control.ts +39 -0
  111. package/src/forms/controls/form-header.ts +16 -0
  112. package/src/forms/controls/form-paragraph.ts +11 -0
  113. package/src/forms/controls/frame.ts +26 -0
  114. package/src/forms/controls/simple-input.ts +73 -0
  115. package/src/forms/controls.ts +0 -0
  116. package/src/forms/field.ts +119 -0
  117. package/src/forms/form-fields-list.ts +7 -0
  118. package/src/forms/form.ts +242 -0
  119. package/src/forms/forms.scss +18 -0
  120. package/src/forms/validator.ts +10 -0
  121. package/src/generate-routes.ts +12 -9
  122. package/src/index.ts +15 -28
  123. package/src/page/access-control.ts +18 -0
  124. package/src/page/list.ts +214 -0
  125. package/src/page/page.ts +31 -0
  126. package/src/page/props-parser.ts +99 -0
  127. package/src/run.ts +0 -0
  128. package/src/templates/form.ts +10 -5
  129. package/src/templates/list.ts +3 -3
  130. package/src/templates/page.ts +6 -6
  131. package/src/utils/escape-url-params.ts +1 -0
  132. package/src/utils/list-collections.ts +2 -2
  133. package/src/utils/natural-numbers.ts +5 -0
  134. package/src/utils/sleep.ts +3 -0
  135. package/src/utils/url-with-new-params.ts +9 -0
  136. package/src/utils/utils.ts +10 -0
  137. package/src/utils/walk.ts +2 -2
  138. package/tsconfig.json +5 -2
  139. package/.log/ti-101379.log +0 -524
  140. package/.log/ti-131883.log +0 -505
  141. package/.log/ti-13490.log +0 -652
  142. package/.log/ti-138140.log +0 -505
  143. package/.log/ti-138288.log +0 -526
  144. package/.log/ti-46589.log +0 -675
  145. package/.log/tsserver.log +0 -1666
  146. 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,11 @@
1
+ import { FormControl } from "./controls";
2
+
3
+ export class FormParagraph extends FormControl {
4
+ role = <const>"decoration";
5
+ constructor(public text: string) {
6
+ super();
7
+ }
8
+ render() {
9
+ return `<p>${this.text}</p>`;
10
+ }
11
+ }
@@ -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,7 @@
1
+ import { FormField } from "./field";
2
+
3
+ export class FormFieldsList {
4
+ static getField(fields: FormField[], name: string) {
5
+ return fields.find((f) => f.name == name);
6
+ }
7
+ }
@@ -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,18 @@
1
+ .form-message {
2
+ --color: black;
3
+ border: 1px solid var(--color);
4
+ border-radius: 5px;
5
+ padding: 10px;
6
+
7
+ &--error {
8
+ --color: #ff4136;
9
+ }
10
+
11
+ &--success {
12
+ --color: #2ecc40;
13
+ }
14
+ }
15
+
16
+ .form__error {
17
+ color: #ff4136;
18
+ }
@@ -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
+ }
@@ -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 { walk } from "./utils/walk";
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 walk(target_locreq.resolve("src/back/routes"))
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
- #!/usr/bin/env node
1
+ import Sealious from "sealious";
2
2
 
3
- import { addCollection } from "./add-collection";
4
- import { addRoute } from "./add-route";
5
- import { generateScssIncludes } from "./find-scss-includes";
6
- import { generateCollections } from "./generate-collections";
7
- import { generateRoutes } from "./generate-routes";
3
+ declare module "koa" {
4
+ interface BaseContext {
5
+ $context: Sealious.Context;
6
+ $body: Record<string, unknown>;
7
+ }
8
+ }
8
9
 
9
- const actions: Record<string, () => Promise<void> | undefined> = {
10
- "add-collection": addCollection,
11
- "add-route": addRoute,
12
- "generate-collections": generateCollections,
13
- "generate-routes": generateRoutes,
14
- "generate-scss-includes": generateScssIncludes,
15
- default: async function () {
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";
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
+ }