@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.
Files changed (139) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.log/ti-292609.log +508 -0
  3. package/.log/ti-304933.log +524 -0
  4. package/.log/ti-312312.log +508 -0
  5. package/.log/ti-314240.log +1617 -0
  6. package/.log/ti-62627.log +508 -0
  7. package/.log/ti-64785.log +508 -0
  8. package/.log/ti-65911.log +533 -0
  9. package/.log/ti-69885.log +548 -0
  10. package/.log/ti-72106.log +551 -0
  11. package/.log/ti-74858.log +508 -0
  12. package/.log/tsserver.log +1610 -1625
  13. package/@types/build.d.ts +1 -0
  14. package/@types/cli.d.ts +2 -0
  15. package/@types/forms/controls/checkboxed-list-input.d.ts +16 -0
  16. package/@types/forms/controls/controls.d.ts +9 -0
  17. package/@types/forms/controls/dropdown.d.ts +20 -0
  18. package/@types/forms/controls/editable-collection-subset.d.ts +21 -0
  19. package/@types/forms/controls/form-control.d.ts +8 -0
  20. package/@types/forms/controls/form-field-control.d.ts +13 -0
  21. package/@types/forms/controls/form-header.d.ts +9 -0
  22. package/@types/forms/controls/form-paragraph.d.ts +7 -0
  23. package/@types/forms/controls/frame.d.ts +17 -0
  24. package/@types/forms/controls/simple-input.d.ts +28 -0
  25. package/@types/forms/controls.d.ts +0 -0
  26. package/@types/forms/field.d.ts +42 -0
  27. package/@types/forms/form-fields-list.d.ts +4 -0
  28. package/@types/forms/form.d.ts +42 -0
  29. package/@types/forms/validator.d.ts +3 -0
  30. package/@types/index.d.ts +14 -2
  31. package/@types/page/access-control.d.ts +6 -0
  32. package/@types/page/list.d.ts +59 -0
  33. package/@types/page/page.d.ts +26 -0
  34. package/@types/page/props-parser.d.ts +20 -0
  35. package/@types/run.d.ts +0 -0
  36. package/@types/utils/natural-numbers.d.ts +1 -0
  37. package/@types/utils/sleep.d.ts +1 -0
  38. package/@types/utils/url-with-new-params.d.ts +2 -0
  39. package/@types/utils/utils.d.ts +9 -0
  40. package/@types/utils/walk.d.ts +1 -1
  41. package/lib/build.js +108 -0
  42. package/lib/build.js.map +1 -0
  43. package/lib/cli.js +30 -0
  44. package/lib/cli.js.map +1 -0
  45. package/lib/find-scss-includes.js +2 -1
  46. package/lib/find-scss-includes.js.map +1 -1
  47. package/lib/forms/controls/checkboxed-list-input.js +43 -0
  48. package/lib/forms/controls/checkboxed-list-input.js.map +1 -0
  49. package/lib/forms/controls/controls.js +22 -0
  50. package/lib/forms/controls/controls.js.map +1 -0
  51. package/lib/forms/controls/dropdown.js +40 -0
  52. package/lib/forms/controls/dropdown.js.map +1 -0
  53. package/lib/forms/controls/editable-collection-subset.js +73 -0
  54. package/lib/forms/controls/editable-collection-subset.js.map +1 -0
  55. package/lib/forms/controls/form-control.js +7 -0
  56. package/lib/forms/controls/form-control.js.map +1 -0
  57. package/lib/forms/controls/form-field-control.js +22 -0
  58. package/lib/forms/controls/form-field-control.js.map +1 -0
  59. package/lib/forms/controls/form-header.js +18 -0
  60. package/lib/forms/controls/form-header.js.map +1 -0
  61. package/lib/forms/controls/form-paragraph.js +16 -0
  62. package/lib/forms/controls/form-paragraph.js.map +1 -0
  63. package/lib/forms/controls/frame.js +29 -0
  64. package/lib/forms/controls/frame.js.map +1 -0
  65. package/lib/forms/controls/simple-input.js +50 -0
  66. package/lib/forms/controls/simple-input.js.map +1 -0
  67. package/lib/forms/controls.js +1 -0
  68. package/lib/forms/controls.js.map +1 -0
  69. package/lib/forms/field.js +80 -0
  70. package/lib/forms/field.js.map +1 -0
  71. package/lib/forms/form-fields-list.js +10 -0
  72. package/lib/forms/form-fields-list.js.map +1 -0
  73. package/lib/forms/form.js +162 -0
  74. package/lib/forms/form.js.map +1 -0
  75. package/lib/forms/validator.js +11 -0
  76. package/lib/forms/validator.js.map +1 -0
  77. package/lib/generate-routes.js +1 -1
  78. package/lib/generate-routes.js.map +1 -1
  79. package/lib/index.js +17 -25
  80. package/lib/index.js.map +1 -1
  81. package/lib/page/access-control.js +20 -0
  82. package/lib/page/access-control.js.map +1 -0
  83. package/lib/page/list.js +148 -0
  84. package/lib/page/list.js.map +1 -0
  85. package/lib/page/page.js +20 -0
  86. package/lib/page/page.js.map +1 -0
  87. package/lib/page/props-parser.js +74 -0
  88. package/lib/page/props-parser.js.map +1 -0
  89. package/lib/run.js +1 -0
  90. package/lib/run.js.map +1 -0
  91. package/lib/templates/list.js +1 -1
  92. package/lib/utils/list-collections.js +1 -1
  93. package/lib/utils/list-collections.js.map +1 -1
  94. package/lib/utils/natural-numbers.js +10 -0
  95. package/lib/utils/natural-numbers.js.map +1 -0
  96. package/lib/utils/sleep.js +8 -0
  97. package/lib/utils/sleep.js.map +1 -0
  98. package/lib/utils/url-with-new-params.js +12 -0
  99. package/lib/utils/url-with-new-params.js.map +1 -0
  100. package/lib/utils/utils.js +19 -0
  101. package/lib/utils/utils.js.map +1 -0
  102. package/lib/utils/walk.js +4 -4
  103. package/lib/utils/walk.js.map +1 -1
  104. package/package.json +17 -4
  105. package/src/build.ts +82 -0
  106. package/src/cli.ts +34 -0
  107. package/src/find-scss-includes.ts +3 -2
  108. package/src/forms/controls/checkboxed-list-input.ts +50 -0
  109. package/src/forms/controls/controls.ts +9 -0
  110. package/src/forms/controls/dropdown.ts +57 -0
  111. package/src/forms/controls/editable-collection-subset.ts +91 -0
  112. package/src/forms/controls/form-control.ts +13 -0
  113. package/src/forms/controls/form-field-control.ts +39 -0
  114. package/src/forms/controls/form-header.ts +16 -0
  115. package/src/forms/controls/form-paragraph.ts +11 -0
  116. package/src/forms/controls/frame.ts +26 -0
  117. package/src/forms/controls/simple-input.ts +73 -0
  118. package/src/forms/controls.ts +0 -0
  119. package/src/forms/field.ts +119 -0
  120. package/src/forms/form-fields-list.ts +7 -0
  121. package/src/forms/form.ts +242 -0
  122. package/src/forms/forms.scss +18 -0
  123. package/src/forms/validator.ts +10 -0
  124. package/src/generate-routes.ts +2 -2
  125. package/src/index.ts +14 -28
  126. package/src/page/access-control.ts +18 -0
  127. package/src/page/list.ts +214 -0
  128. package/src/page/page.ts +31 -0
  129. package/src/page/props-parser.ts +99 -0
  130. package/src/run.ts +0 -0
  131. package/src/templates/list.ts +1 -1
  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/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,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) =>
package/src/index.ts CHANGED
@@ -1,30 +1,16 @@
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";
@@ -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
+ }
@@ -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
+ }